Practical .NET
Use Predicate Methods to Stop Writing For...Each Loops
Predicate methods allow you to separate the criteria you're using to test items in a collection from the code that processes the collection. You may never write a For...Each loop again.
Here are some typical things you need to do with Lists: You need to retrieve some item (or items) from the List; you need to remove some items from the List; or you need to check to make sure that all the items in the List meet some condition. For all of these problems, you might normally begin by writing a For...Each loop. But if you do that, you might be writing too much code. There are methods built into the List that will take care of some of the code for you.
For instance, the following code creates a collection of Customer, specifying the Customer ID for each Customer object:
Dim sList As New List(Of Customer)
Dim cust As Customer
cust = New Customer("Q456")
sList.Add(cust)
cust = New Customer("A123")
sList.Add(cust)
cust = New Customer("0789")
sList.Add(cust)
The most obvious thing you might want to do is find a Customer object with a specific ID. You could do so by writing code like this:
Dim custFound As Customer
For Each cust In custList
If cust.CustID = "A123" Then
custFound = cust
Exit For
End If
Next
One way of looking at the preceding code is to break it into two groups. First, you have the utility code that handles looping through the collection, selecting the item and exiting the loop -- you write that code every time you process the items in a List. Second, there's the code that's unique to this problem: the code that does the test. As far as I'm concerned, that's at least two separate parts I can get wrong. More important, the only part where I'm really adding any value is in the test.
Integrating Predicate Methods
Rather than write both parts of that code, I can use the List's Find method to take care of all the code except for the test, leaving me only one part to get wrong. To use the Find method, I start by writing the test in a separate method, as shown here:
Dim IdToFind As String
Public Function MatchCustomer(Cust As Customer) As Boolean
If Cust.ID = IdToFind Then Return True
Return False
End Function
Now, in my code, I can just pass my test method (which Microsoft calls a predicate) to the List's Find method. I don't even have to specify the Customer object that I pass as a parameter to my predicate method -- the Find method will pick each item out of the List and pass the item to the method:
IdToFind = "A123"
Dim cust As Customer = custList.Find(AddressOf MatchCustomer)
I like this new code for two reasons: First, it separates out the test from the utility code and eliminates the utility code altogether (including the extra custFound variable). Second, having set my predicate up, I can use it with several other methods on the List that accept a predicate.
For instance, the Find method has one significant limitation: It will only return the first matching item. If I know there are several matching items, I can switch to the FindAll method (which returns a List of all the matching items) and still use the same predicate. Rewriting the predicate-based version of my code is simple: I just change the method name from Find to FindAll and change the datatype of the variable that holds my results. I can still use my predicate:
Dim custsFound As List(Of Customer) =
custList.FindAll(AddressOf MatchCustomer)
It gets better: In addition to the Find and FindAll methods, you can also use predicates with the FindIndex method (which returns the position of the matching object), FindLast and FindLastIndex. The RemoveAll method lets you use the predicate to find and delete all the matching items. My favorite predicate method is TrueForAll, which returns True when all of the items in the collection return True from the predicate method, giving me a quick way of checking that all the items meet some validity test.
Dealing with Parameters
Unfortunately, there's no easy way to pass a parameter of my own to a predicate, so I've declared the IdToFind variable as a "class-level" variable, outside of any method. Passing a value to a predicate method isn't always necessary -- some predicates will just crosscheck values on the object while other predicates will test against constant values. However, sometimes you need to pass a parameter to your predicate, as in my example.
If you don't like class-level variables, you can eliminate them. First, if you're not going to use a predicate anywhere else, you can pass your predicate as a lambda expression and use a local variable within the expression. Using a lambda expression in the example would give this code:
Dim IdToFind As String = "A123"
Dim custsFound As List(Of Customer) =
custList.FindAll(Function(c)
If c.ID = IdToFind Then Return True
Return False
End Function)
Alternatively, you can create a separate class to hold your predicate (and any other predicates that you might create) and give it a constructor that accepts your parameter. Here's the class to support my example:
Public Class CustomerPredicates
Private IdToFind As String
Public Sub New(IdToFind As String)
Me.IdToFind = IdToFind
End Sub
Public Function MatchCustomer(Cust As Customer) As Boolean
If Cust.ID = IdToFind Then Return True
Return False
End Function
End Class
Now, when you call your predicate method, you instantiate the class and pass your parameter to the class' constructor:
Dim custsFound As List(Of Customer) =
custList.FindAll(
AddressOf New CustomerPredicates("A127").MatchCustomer)
Understanding predicates lets you exploit useful methods on the List collection. And, if you look around, you'll find that IntelliSense will show you these are just the beginning of what you can do with predicates.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.