generic delegate creation using Expression in C#
Asked Answered
N

2

5

Given below are two methods which create a delegate to set a field in a class. One method uses generics and the other does not. Both the methods return a delegate and they work fine. But if I try to use the delegate that has been created inside the CreateDelegate method, then the non-generic delegate 'del' works fine. I can place a breakpoint on the return statement and invoke the delegate by writting del(222). But If I try to invoke the generic delegate 'genericDel' by writting genericDel(434), it throws an exception:

Delegate 'System.Action' has some invalid arguments

Can anyone explain this quirk.

class test
{
    public double fld = 0;
}

public static void Main(string[] args)
{
    test tst = new test() { fld = 11 };

    Type myType = typeof(test);
    // Get the type and fields of FieldInfoClass.
    FieldInfo[] myFieldInfo = myType.GetFields(BindingFlags.Instance | BindingFlags.Public);
    var a = CreateDelegate<double>(myFieldInfo[0], tst);
    var b = CreateDelegate(myFieldInfo[0], tst);

    Console.WriteLine(tst.fld);

    b(5.0);
    Console.WriteLine(tst.fld);

    a(6.0);
    Console.WriteLine(tst.fld);
}

public static Action<T> CreateDelegate<T>(FieldInfo fieldInfo, object instance)
{
    ParameterExpression numParam = Expression.Parameter(typeof(T), "num");
    Expression a = Expression.Field(Expression.Constant(instance), fieldInfo);
    BinaryExpression assExp = Expression.Assign(a, numParam);

    Expression<Action<T>> expTree =
        Expression.Lambda<Action<T>>(assExp,
            new ParameterExpression[] { numParam });

    Action<T> genericDel = expTree.Compile();
    //try to invoke the delegate from immediate window by placing a breakpoint on the return below: genericDel(323)
    return genericDel;
}

public static Action<double> CreateDelegate(FieldInfo fieldInfo, object instance)
{
    ParameterExpression numParam = Expression.Parameter(typeof(double), "num");
    Expression a = Expression.Field(Expression.Constant(instance), fieldInfo);
    BinaryExpression assExp = Expression.Assign(a, numParam);

    Expression<Action<double>> expTree =
        Expression.Lambda<Action<double>>(assExp,
            new ParameterExpression[] { numParam });

    Action<double> del = expTree.Compile();
    //try to invoke the delegate from immediate window by placing a breakpoint on the return below: del(977)
    return del;
}
Nicky answered 22/10, 2011 at 6:55 Comment(4)
I have tried both a(5.0) and b(5.0) and they work correctly. Be aware that this code is C# 4.0 (the Expression.Assign was introduced there)Cletacleti
Can you show us a complete example that demonstrates the problem? How are you invoking the delegates?Teens
Ah... I have added the calls to a() and b() in your code, so the code is directly testable.Cletacleti
Well, that is what I am saying a() and b() would work fine. But try to invoke del() and genericDel()(by placing a breakpoint before the CreateDelegate method returns) and you note that genericDel() throws the said exception while del() works fine.Nicky
T
7

I think I understood the issue; you are having problems invoking a generic delegate from the immediate window when the compile-time type of the delegate is an open generic type. Here's a simpler repro:

  static void Main() { Test<double>(); }

  static void Test<T>()
  {
        Action<T> genericDel = delegate { };
       // Place break-point here.
  }

Now, if I try executing this delegate from within the Test method (by placing a break-point and using the immediate window) like this:

genericDel(42D);

I get the following error:

Delegate 'System.Action<T>' has some invalid arguments

Note that this not an exception like you have stated, but rather the 'immediate window version' of compile-time error CS1594.

Note that such a call would have failed equally at compile-time because there is no implicit or explicit conversion from double to T.

This is debatably a shortcoming of the immediate window (it doesn't appear to be willing to use additional 'run-time knowledge' to help you out in this case), but one could argue that it is reasonable behaviour since an equivalent call made at compile-time (in source code) would also have been illegal. This does appear to be a corner case though; the immediate window is perfectly capable of assigning generic variables and executing other code that would have been illegal at compile-time. Perhaps Roslyn will make things much more consistent.

If you wish, you can work around this like so:

genericDel.DynamicInvoke(42D);

(or)

((Action<double>)(object)genericDel)(42D);
Teens answered 22/10, 2011 at 7:45 Comment(2)
Hey Ani, Yes that is the issue. I had tried it with the DynamicInvoke call(as u mention here) but I was trying to understand the reason why it was not working in code as it is written. As you say here, it might be a bug. And by the way, your second tip is a different way to solve it:((Action<double>)(object)genericDel)(3434); I did not knew that.Nicky
Is there a drawback in using DynamicInvoke to solve the problem?Reisinger
A
0

The problem is that you are trying to invoke the delegate within the scope of the method that is creating it, before 'T' is known. It is trying to convert a value type (an integer) to the generic type 'T', which is not allowed by the compiler. If you think about it, it makes sense. You should only be able to pass in T as long as you are within the scope of the method that is creating the delegate, otherwise it wouldn't really be generic at all.

You need to wait for the method to return, then use the delegate. You should have no problem invoking the delegate after its completed:

var a = CreateDelegate<double>(myFieldInfo[0], tst);     
var b = CreateDelegate(myFieldInfo[0], tst); 

a(434); 
Acierate answered 22/10, 2011 at 7:26 Comment(6)
And why is that so. Is it a documented behaviour? I mean, when the control is inside the method, doesn't the method know what 'T' is. For example, we have already used 'T' to define numParam.Nicky
Because T is a placeholder for any type. In fact 'numParam' isn't gauranteed to be a number at all, its only gauranteed to be of type T. Thats the whole point of a generic argument. Its really a misnomer to label that variable 'numParam' -- it could (and should) be capable of being any type, otherwise its not generic.Acierate
When you call the CreateDelegate<double>, in that case if you place a breakpoint on numParam you would note that 'T' has been replaced by double. Here is the type for numParam fetched from QuickWatch:- "Type = {Name = "Double" FullName = "System.Double"}". So 'T' has been instantiated in the call.Nicky
The error you are getting occurs at compile time in order to prevent you from doing something that would otherwise cause problems at runtime. At compile time, the type of 'T' cannot and should not be known within the scope of the method, otherwise generics wouldn't work. Imagine if you tried to invoke genericDel(434) inside the method when you passed in a completely different generic parameter such as CreateDelegate<String> -- that would throw a run time error if you didn't get the compile time check in advance. Because you would be trying to convert an int into a string implicitly.Acierate
Hey mate...I am not saying that I am making the call at compile time. Run the program, place a breakpoint before the createdelegate method returns and fire off the delegate in immmediate window.Nicky
Ah, I see what you mean now. Thats a bit different, but I'd still chalk it up to the fact that 'T' has to remain generic as long as you are inside the scope of the generic method. The DLR has resolved the type but the CLR hasn't and still expects 'T' as an argument. Notice that under the hood .NET is using CompilerServices.Closure to make this work. That suggests that the closed over variables are bound into the scope of the dynamic expression tree and not necessarily (by default) to the outer method that is generating it. As Ani already mentioned maybe Roslyn will address these issues..Acierate

© 2022 - 2024 — McMap. All rights reserved.