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

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube