Blog Archive

Friday, June 30

C# 3.0 and Delegate Conversion

System.Query defines a number of delegate types to help with query expressions. They follow the pattern of:

delegate T Func<T>();
delegate T Func<A0, T>(A0 arg0);
delegate T Func<A0, A1, T>(A0 arg0, A1 arg1);
// etc.

This is all well and good.

Certain methods in System.Query.Sequence take a parameter of Func<T, bool>. An example is Where<T>(this IEnumerable<T> source, Func<T, bool> predicate). Note that the underlying signatures of Func<T, bool> and Predicate<T> match.

Well it turns out that Delegates are kind of funky. For one thing, the Delegate class cannot be used as a generic type constraint. Also, conversion between compatible types of delegates isn't allowed (that is, you can't cast them).

That means that if one has a reference to an existing Predicate<T> one cannot use it as an argument to Where. I'm sure it won't come up much, because it's not often that one has a reference to a delegate without being able to reach the method that the delegate referrs to, but it could happen.

The best way I came up with to convert between two types of compatible delegates:

// CS0702: Constraint cannot be special class 'System.Delegate'
public static T ConvertTo<T>(this Delegate source)
    where T : class //, Delegate
{
    if (source.GetInvocationList().Length > 1)
        throw new ArgumentException("Cannot safely convert MulticastDelegate");

    return Delegate.CreateDelegate(typeof(T), source.Target, source.Method) as T;
}

This isn't great. Something like the following compiles just fine:

Predicate<int> isEven = n => n % 2 == 0;
MailAddress email = isEven.ConvertTo<MailAddress>();

Since Delegate is invalid as a type constraint, there's not really any way around it. I don't think that will ever cause any difficulty, though.

More subtly evil, however, is that conversion between incompatible delegates will appear to work fine at compile time, but result in a runtime error. Oh well. At least now I can do this:

int[] numbers = { 1, 2, 3, 4, 5 };
Predicate<int> isEven = n => n % 2 == 0;
foreach (var n in numbers.Where(isEven.ConvertTo<Func<int, bool>>()))
    Console.WriteLine(n);

// Output:
// 2
// 4

… not that I ever would…

Update: the delegate conversion method has been extended/updated.

4 comments:

Zaknafein said...

Excellent, thanks for that! I wonder why methods like HashSet<T>.RemoveWhere takes a predicate when every Enumerable extension method work with Func<T...>'s. They were all added at the same time with .NET 3.5!

Steve Guidi said...

I've written a functor library that may help address this problem. See http://jolt.codeplex.com for more information.

RRave said...

Dear Sir,

I have a launched new web site for .NET programming resources. www.codegain.com. I would like to invite to the codegain.com as author and supporter. I hope you will joins with us soon.

Thank You
RRaveen
Founder www.codegain.com

Anonymous said...
This comment has been removed by a blog administrator.