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/.

comments powered by Disqus

Featured

  • IDE Irony: Coding Errors Cause 'Critical' Vulnerability in Visual Studio

    In a larger-than-normal Patch Tuesday, Microsoft warned of a "critical" vulnerability in Visual Studio that should be fixed immediately if automatic patching isn't enabled, ironically caused by coding errors.

  • Building Blazor Applications

    A trio of Blazor experts will conduct a full-day workshop for devs to learn everything about the tech a a March developer conference in Las Vegas keynoted by Microsoft execs and featuring many Microsoft devs.

  • Gradient Boosting Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the gradient boosting regression technique, where the goal is to predict a single numeric value. Compared to existing library implementations of gradient boosting regression, a from-scratch implementation allows much easier customization and integration with other .NET systems.

  • Microsoft Execs to Tackle AI and Cloud in Dev Conference Keynotes

    AI unsurprisingly is all over keynotes that Microsoft execs will helm to kick off the Visual Studio Live! developer conference in Las Vegas, March 10-14, which the company described as "a must-attend event."

  • Copilot Agentic AI Dev Environment Opens Up to All

    Microsoft removed waitlist restrictions for some of its most advanced GenAI tech, Copilot Workspace, recently made available as a technical preview.

Subscribe on YouTube