tags:

views:

939

answers:

7

Using C# 3.5 I wanted to build up a predicate to send to a where clause piece by piece. I have created a very simple Console Application to illustrate the solution that I arrived at. This works perfectly. Absolutely perfectly. But I have NO idea how or why.

    public static Func<Tran, bool> GetPredicate()
    {
        Func<Tran, bool> predicate = null;
        predicate += t => t.Response == "00";
        predicate += t => t.Amount < 100;
        return predicate;
    }

When I say 'predicate +=', what does that mean? predicate -= appears to do nothing and ^=, &=, *=, /= aren't liked by the compiler.

The compiler doesn't like 'predicate = predicate + t => t.Response....' either.

What have I stumbled on? I know what it does, but how does it do it?

If anyone wants to go delve into more complicated lambda's, please do so.

A: 

The += is syntactic sugar specifically implemented to support adding handlers to events. Since events are just a special case of delegate, and Func is also a delegate, the syntax appears to work here.

But are you sure it works as expected? By that I mean, do you expect an AND or OR evaluation? How would you implement the opposite if you wanted it? Are you sure it's not just returning the result of the first? Or the last?

Joel Coehoorn
+12  A: 

Actually, that doesn't work. Try to test it with a case where the first condition FAILS but the second one passes. You'll find that it'll return true. The reason is, because when dealing with multicast delegates that return values, only the last value is returned. For example:

        Func<string, bool> predicate = null;
        predicate += t => t.Length > 10;
        predicate += t => t.Length < 20;

        bool b = predicate("12345");

This will return TRUE because the last function call returns true (it's less than 20). In order to really make it work, you need to call:

predicate.GetInvocationList();

which returns an array of delegates. You then need to make sure they ALL return true, for the final result to be true. Make sense?

BFree
+10  A: 

When you use += or -= when delegate types, that just gives a call to Delegate.Combine and Delegate.Remove.

The important thing about multicast delegates is that the return value of all but the last executed delegate is ignored. They all get executed (unless an exception is thrown), but only the last return value is used.

For predicates, you might want to do something like:

public static Func<T, bool> And<T>(params Func<T, bool>[] predicates)
{
    return t => predicates.All(predicate => predicate(t));
}

public static Func<T, bool> Or<T>(params Func<T, bool>[] predicates)
{
    return t => predicates.Any(predicate => predicate(t));
}

You'd then do:

Func<string, bool> predicate = And<string>(
    t => t.Length > 10,
    t => t.Length < 20);

EDIT: Here's a more general solution which is quite fun, if a bit bizarre...

public static Func<TInput, TOuput> Combine<TInput, TOutput>
    (params Func<TInput, TOuput>[] delegates,
     Func<TOutput, TOutput, TOutput> aggregator) {

    // delegates[0] provides the initial value
    return t => delegates.Skip(1).Aggregate(delegates[0](t), aggregator);
}

So you could then implement And as:

public static Func<T, bool> And<T>(params Func<T, bool>[] predicates) {
    return Combine<T, bool>(predicates, (x, y) => x && y);
}

(I personally prefer this over using GetInvocationList(), because you end up with a predicate you can pass to other bits of LINQ etc.)

Jon Skeet
Oh cool, I never knew about All and Any. You're right, this is much better than GetInvocationList(). Guess there's still some .NET 2.0 left in me...
BFree
I would suggest AndAlso and OrElse for the names - it makes it more consistent for VB users and lets them know that the methods short-circuit.
Keith
@Keith: I agree that the names aren't ideal, but I don't think AndAlso/OrElse are better at the moment. TrueForAll and TrueForAny would be better, I suspect.
Jon Skeet
Wow! you post hit before me for 4 minutes. In fact, I see message for refresh while I edit my answer. But I don't think the message will be the same :)
chaowman
+2  A: 

Expanding on BFree's answer (+1'd it)

If you want to get the behavior you're looking for, you'll need to explicitly chain the predicates together. Here is an example

public static Func<Tran, bool> GetPredicate()
{
    Func<Tran, bool> predicate1 = t => t.Response == "00";
    Func<Tran, bool> predicate2 = t => t.Amount < 100;
    return t => predicate1(t) && predicate2(t);
}
JaredPar
A: 

This blog post provides some interesting background.

Samuel Jack
+8  A: 

"delegate += method" is operator for multicast delegate to combine method to delegate. In the other hand "delegate -= method" is operator to remove method from delegate. It is useful for Action.

Action action = Method1;
action += Method2;
action += Method3;
action -= Method2;
action();

In this case, only Method1 and Method3 will run, Method2 will not run because you remove the method before invoke the delegate.

If you use multicast delegate with Func, the result will be last method.

Func<int> func = () => 1;
func += () => 2;
func += () => 3;
int result = func();

In this case, the result will be 3, since method "() => 3" is the last method added to delegate. Anyway all method will be called.

In your case, method "t => t.Amount < 100" will be effective.

If you want to combine predicate, I suggest these extension methods.

public static Func<T, bool> AndAlso<T>(this Func<T, bool> predicate1, Func<T, bool> predicate2) {
    return arg => predicate1(arg) && predicate2(arg);
}
public static Func<T, bool> OrElse<T>(this Func<T, bool> predicate1, Func<T, bool> predicate2) {
    return arg => predicate1(arg) || predicate2(arg);
}

Usage

public static Func<Tran, bool> GetPredicate() {
    Func<Tran, bool> predicate = null;
    predicate = t => t.Response == "00";
    predicate = predicate.AndAlso(t => t.Amount < 100);
    return predicate;
}

EDIT: correct the name of extension methods as Keith suggest.

chaowman
The Skeet beat you to it, but I like the extension method combinations. I would suggest AndAlso and OrElse for the names tho' - it makes it more consistent for VB users and lets them know that the methods short-circuit.
Keith
A: 

If you want to combine predicates, try this;

public static Predicate<T> Combine<T>(params Predicate<T>[] predicates)
{
return t => predicates.All(pr => pr(t));
}

You call it like this;

Predicate<string> shortAndSweet = Combine<string>
(
    s => s.Length < 10,  // short,
    s => s == "sweet"    // and sweet
);
Steve Cooper