Combine Multiple Predicates

eric picture eric · Aug 8, 2009 · Viewed 28.9k times · Source

Is there any way in c# .NET 2.0! to combine multiple Predicates?

Let's say I have the following code.

List<string> names = new List<string>();
names.Add("Jacob");
names.Add("Emma");
names.Add("Michael");
names.Add("Isabella");
names.Add("Ethan");
names.Add("Emily");

List<string> filteredNames = names.FindAll(StartsWithE);

static bool StartsWithE(string s)
{
    if (s.StartsWith("E"))
    {
        return true;
    }
    else
    {
        return false;
    }
}

This gives me:

Emma
Ethan
Emily

So this is pretty cool stuff, but I know want to be able to filter using multiple predicates.

So I want to be able to say something like this:

List<string> filteredNames = names.FindAll(StartsWithE OR StartsWithI);

In order to get:

Emma
Isabella
Ethan
Emily

How can I achieve this? Currently I am just filtering the complete list twice and combining the results afterwards. But unfortunately this is quite inefficent and even more importantly I lose the original sort order, which is not acceptable in my situation.

I also need to be able to iterate over any number of filters/predicates as there can be quite a lot.

Again it needs to be a .NET 2.0 solution unfortunately I can't use a newer version of the framework

Thanks a lot.

Answer

Jon Skeet picture Jon Skeet · Aug 8, 2009

How about:

public static Predicate<T> Or<T>(params Predicate<T>[] predicates)
{
    return delegate (T item)
    {
        foreach (Predicate<T> predicate in predicates)
        {
            if (predicate(item))
            {
                return true;
            }
        }
        return false;
    };
}

And for completeness:

public static Predicate<T> And<T>(params Predicate<T>[] predicates)
{
    return delegate (T item)
    {
        foreach (Predicate<T> predicate in predicates)
        {
            if (!predicate(item))
            {
                return false;
            }
        }
        return true;
    };
}

Then call it with:

List<string> filteredNames = names.FindAll(Helpers.Or(StartsWithE, StartsWithI));

Another alternative would be to use multicast delegates and then split them using GetInvocationList(), then do the same thing. Then you could do:

List<string> filteredNames = names.FindAll(Helpers.Or(StartsWithE+StartsWithI));

I'm not a huge fan of the latter approach though - it feels like a bit of an abuse of multicasting.