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

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube