Wednesday, June 21, 2006

The Power of Predicates

I ran up against a problem I'd not encountered before today - how to use the Find method on a generic List. The MSDN documentation is less than enlightening, but I found an excellent article entitled "Using generics to manage a collection of objects" byAmirthalingam Prasanna on simple-talk.com.

Amirthalingam covers the subject in detail, but I thought I'd share my reference implementation (and put it up somewhere that I can find it when I forget how to do this!). Purple elipses ( ... ) indicate where the finished code continues.

We'll implement a simple C# console application to start with to act as the framework for our reference implementation.
using System;
using System.Collections.Generic;
using System.Text;

namespace List_Find_Predicate_Test
{
class Program
{
static void Main(string[] args)
{
}
}
}
Next, let's define a "complex" class against a List of which we will search - we'll do this as a private inner class within our framework:
...
class Program
{
private enum Gender
{
Male,
Female
}

private class Criteria
{
public string Field;
public Type DataType;

public Criteria(string theField, Type theDataType)
{
this.Field = theField;
this.DataType = theDataType;
}
}
...
}
...
You'll notice I've spiced things up with a custom enumeration used within the Criteria class. Next, let's add some code to the Main method to build us a list of criteria objects:
...
static void Main(string[] args)
{
List<Criteria> critList = new List<Criteria>();
critList.Add(new Criteria("Name", typeof(System.String)));
critList.Add(new Criteria("Age", typeof(System.Int16)));
critList.Add(new Criteria("Gender", typeof(Gender)));

Console.WriteLine("List has {0} entries...", critList.Count);

// TODO: Search the list
...
}
...
Simple enough so far. The complexity happens in providing a mechanism to let the List.Find method operate - this is where Predicate classes come in. Predicates are analagous to Delegates - they effectively implement a call-back to a method that performs the comparison, and it is this call-back that the Find method uses to find matching elements in the List.

Let's add a quick search of the List, and get the type assigned to the "Name" criteria:
...
static void Main(string[] args)
{
...
// Search the list
Criteria crit = FindCriteriaByName(critList, "Name");
Console.WriteLine("Name criteria has type {0}", crit.DataType.Name);

// TODO: Search without having to use a helper method
...
}

// TODO: Implement FindCriteriaByName
...
OK - that was easy enough - just your common-or-garden-variety top-down programming so far. But now we get to the meat of the issue - the implementation of the FindCriteriaByName method - and this is where Predicates are used.

The key to Predicates is that they reference a class - in this case a class that is dedicated to filtering a List. We'll implement this as another private class within the program framework. This filter class is as simple as they come, taking a string parameter for its constructor, and exposing just one method FilterByField.
...
class Program
{
...
private class CriteriaFieldFilter
{
private string criteriaField;

public CriteriaFieldFilter(string fieldName)
{
criteriaField = fieldName;
}

public bool FilterByField(Criteria crit)
{
return (crit.Field == criteriaField);
}
}
...
}
...
You'll notice that the critical FilterByField method accepts a Criteria object as its parameter - when we call this class through a Predicate, this is the method that will be used to perform the filtering. So we can now finally implement the filter method:
...
class Program
{
...
// Implement FindCriteriaByName
private static Criteria FindCriteriaByName(List<Criteria> theList, string theName)
{
CriteriaFieldFilter searchFilter = new CriteriaFieldFilter(theName);
Predicate<Criteria> searchPredicate = new Predicate<Criteria>(searchFilter.FilterByField);
Criteria crit = theList.Find(searchPredicate);
return crit;
}
...
}
...
So our filtermethod starts by instantiating an instance of our filter class tailored with the field name passed in the theName parameter. The Predicate is then created against a pointer to the FilterByField method of that instance, and in turn that Predicate is used with the List.Find method to return the matching entry.

But is there a more compact way rather than having to implement the helper method at all? Of course there is! We just compact the code into an in-line function, thus:
...
static void Main(string[] args)
{
...
// Search without having to use a helper method
crit = critList.Find(new Predicate<Criteria>((new CriteriaFieldFilter("Gender")).FilterByField));
Console.WriteLine("Gender criteria has type {0}", crit.DataType.Name);
...
}
...
Rather harder on the eye, but more compact. I'd imagine that something could be done with Anonymous classes/methods to simplify this further, but that's a topic of research for another time.

Phew! A complex topic for just performing a Find in a List, but it does allow us to write any kind of matching algorithm we like working against just a single T instance and they use that matching algorithm to work with an entire List - that's the power of Predicates.

No comments: