使用C#中的表达式创建通用委托

5
下面提供了两种方法来创建一个委托以设置类中的字段。其中一种方法使用泛型,另一种则不使用。 这两种方法都返回一个委托,并且它们都可以正常工作。但是如果我尝试使用在CreateDelegate方法内创建的委托,那么非泛型委托“del”能够正常工作。我可以在返回语句上放置断点并通过写入del(222)来调用该委托。但是,如果我尝试通过写入genericDel(434)来调用泛型委托“genericDel”,它会抛出以下异常: “Delegate 'System.Action' has some invalid arguments” 有人可以解释这个怪异的现象吗?
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;
}

我已经尝试过a(5.0)b(5.0),它们都能正常工作。请注意,此代码是C# 4.0版本(其中引入了Expression.Assign)。 - xanatos
你能给我们展示一个完整的例子来演示这个问题吗?你是如何调用委托的? - Ani
啊...我已经在你的代码中添加了对a()b()的调用,所以代码可以直接进行测试。 - xanatos
好的,这就是我所说的a()和b()可以正常工作。但是尝试调用del()和genericDel()(通过在CreateDelegate方法返回之前设置断点),你会注意到genericDel()抛出了上述异常,而del()可以正常工作。 - umbersar
2个回答

7
我认为我理解了问题;您在立即窗口调用泛型委托时遇到了问题,当委托的编译时类型是开放式泛型类型时。以下是一个更简单的复现:
  static void Main() { Test<double>(); }

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

现在,如果我尝试在Test方法中执行此委托(通过放置断点并使用即时窗口),就像这样:
genericDel(42D);

我得到以下错误:
Delegate 'System.Action<T>' has some invalid arguments

请注意,这不是像您所说的异常,而是编译时错误CS1594的“立即窗口版本”。 请注意,这样的调用在编译时同样会失败,因为没有从double到T的隐式或显式转换。 这可能是立即窗口的一个缺点(它似乎不愿意在这种情况下使用额外的“运行时知识”来帮助您),但有人可以认为这是合理的行为,因为在源代码中进行的等效调用也将被视为非法。不过,这似乎是一个特例;立即窗口完全能够分配泛型变量并执行其他在编译时将被视为非法的代码。也许Roslyn会使事情更加一致。
如果您愿意,可以通过以下方式解决此问题:
genericDel.DynamicInvoke(42D);

(或)

((Action<double>)(object)genericDel)(42D);

嗨Ani,是的,那就是问题所在。我已经尝试使用DynamicInvoke调用(正如你在这里提到的),但我正在努力理解为什么它不能按照代码编写的方式工作。正如你在这里所说,这可能是一个错误。顺便说一下,你的第二个提示是另一种解决方法:((Action<double>)(object)genericDel)(3434); 我不知道那个。 - umbersar
使用DynamicInvoke解决问题是否存在缺点? - Force444

0
问题在于您正在尝试在创建委托的方法范围内调用它,在“T”未知之前。它试图将值类型(整数)转换为泛型类型“T”,这是编译器不允许的。如果您考虑一下,这是有道理的。只有在创建委托的方法范围内,您才能传递T,否则它根本不会是通用的。
您需要等待该方法返回,然后使用委托。完成后,您应该没有问题调用委托:
var a = CreateDelegate<double>(myFieldInfo[0], tst);     
var b = CreateDelegate(myFieldInfo[0], tst); 

a(434); 

为什么会这样呢?这是一种已记录的行为吗?我的意思是,当控件在方法内部时,该方法不知道'T'是什么。例如,我们已经使用'T'来定义numParam。 - umbersar
因为T是任何类型的占位符。实际上,'numParam'并不保证是一个数字,它只保证是类型T。这就是泛型参数的全部意义所在。将该变量标记为'numParam'实际上是一个误称--它可以(而且应该)能够成为任何类型,否则它就不是泛型的了。 - Sean Thoman
当您调用CreateDelegate<double>时,如果在numParam上设置断点,则会注意到'T'已被替换为double。以下是从QuickWatch获取的numParam类型:-“Type = {Name =”Double“FullName =”System.Double“}”。因此,在调用中实例化了'T'。 - umbersar
你遇到的错误发生在编译时,以防止你在运行时做一些可能会导致问题的事情。在编译时,方法范围内不能也不应该知道“T”的类型,否则泛型将无法工作。想象一下,如果你尝试在方法内调用genericDel(434),而你传入了完全不同的泛型参数,比如CreateDelegate<String>,那么如果你没有提前进行编译时检查,就会抛出运行时错误。因为你会试图将int隐式转换为string。 - Sean Thoman
嘿,伙计...我并不是说我在编译时进行调用。运行程序,在createdelegate方法返回之前设置断点,并在立即窗口中触发委托。 - umbersar
啊,我现在明白你的意思了。这有点不同,但只要你在通用方法的范围内,我仍然会把它归因于“T”必须保持通用性。DLR已经解析了类型,但CLR还需要将“T”作为参数。请注意,在.NET底层中使用CompilerServices.Closure使其工作。这表明封闭变量被绑定到动态表达式树的范围内,而不一定(默认情况下)绑定到生成它的外部方法。正如Ani已经提到的,也许Roslyn会解决这些问题。 - Sean Thoman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接