如何将空方法调用表示为DynamicMetaObject.BindInvokeMember的结果?

58

我正在尝试为《C#深度探索》第二版提供一个短小的示例,展示 IDynamicMetaObjectProvider 的用法,但遇到了问题。

我想要表达一个 void 调用,但一直失败。我确信这是可能的,因为如果我使用反射绑定器动态调用 void 方法,一切正常。以下是一个简短但完整的示例:

using System;
using System.Dynamic;
using System.Linq.Expressions;

class DynamicDemo : IDynamicMetaObjectProvider
{
    public DynamicMetaObject GetMetaObject(Expression expression)
    {
        return new MetaDemo(expression, this);
    }

    public void TestMethod(string name)
    {
        Console.WriteLine(name);
    }

}

class MetaDemo : DynamicMetaObject
{
    internal MetaDemo(Expression expression, DynamicDemo demo)
        : base(expression, BindingRestrictions.Empty, demo)
    {
    }

    public override DynamicMetaObject BindInvokeMember
        (InvokeMemberBinder binder, DynamicMetaObject[] args)
    {
        Expression self = this.Expression;

        Expression target = Expression.Call
            (Expression.Convert(self, typeof(DynamicDemo)),
             typeof(DynamicDemo).GetMethod("TestMethod"),
             Expression.Constant(binder.Name));

        var restrictions = BindingRestrictions.GetTypeRestriction
            (self, typeof(DynamicDemo));

        return new DynamicMetaObject(target, restrictions);
    }
}

class Test
{
    public void Foo()
    {
    }

    static void Main()
    {
        dynamic x = new Test();
        x.Foo(); // Works fine!

        x = new DynamicDemo();
        x.Foo(); // Throws
    }
}

下面的代码会抛出一个异常:

未处理的异常: System.InvalidCastException: 动态绑定对象 'DynamicDemo' 由绑定程序 'Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder' 生成的类型 'System.Void' 的结果类型与调用位置期望的类型 'System.Object' 不兼容。

如果我把方法改成返回object类型并返回null,那么它就能正常工作...但是我不想让结果为空,我想让它是void。这对于反射绑定器来说没有问题(参见Main中的第一个调用),但对于我的动态对象来说却失败了。我希望它能像反射绑定器一样工作——只要不尝试使用结果,调用该方法就可以了。

我是否错过了可以用作目标的特定表达式?


1
谈论摆下挑战... - Marc Gravell
14
Jon Skeet提出了一个问题,但他自己还没有给出正确的答案。我们感到世界末日的来临。 - Justin Niessner
1
@Marc:我最后一个问题希望能够修复.NET 4.0版本的错误...这次会发生什么谁也不知道 :) - Jon Skeet
1
我们等着瞧 - 我甚至不确定是编译器的问题还是其他什么。无论如何,闲聊够了 - Marc,你比我认识的任何人都更了解表达式树... 我20分钟前就问过这个问题了... 为什么还没有得到答案? :) - Jon Skeet
1
再一次SO拯救了这个局面。我一早上都在和这个问题斗争。+1 - Brian Rasmussen
显示剩余4条评论
4个回答

26

这类似于:

DLR返回类型

您需要匹配由ReturnType属性指定的返回类型。对于所有标准二进制文件,这几乎固定为object或void(用于删除操作)。如果您知道自己在进行void调用,则建议将其包装在中:

Expression.Block(
    call,
    Expression.Default(typeof(object))
);

DLR曾经对允许的内容相当宽松,并自动提供一些最小限度的强制转换。我们放弃了这种做法,因为我们不想为每种语言提供一组可能有意义或可能没有意义的约定。

看起来你想要防止:

dynamic x = obj.SomeMember();

无法实现此功能,始终会返回一个值,用户可以尝试动态交互。


谢谢。看起来基于反射的绑定器可以做到这一点,但我却不能,这有点奇怪,但也不是什么大问题。如果我知道我们不能这样做,那就是我需要警告读者的事情 :) - Jon Skeet

11

我不喜欢这样做,但它似乎可以用;真正的问题似乎是binder.ReturnType出现了奇怪的问题(并且没有被自动删除(“弹出”)),但是:

if (target.Type != binder.ReturnType) {
    if (target.Type == typeof(void)) {
        target = Expression.Block(target, Expression.Default(binder.ReturnType));
    } else if (binder.ReturnType == typeof(void)) {
        target = Expression.Block(target, Expression.Empty());
    } else {
        target = Expression.Convert(target, binder.ReturnType);
    }
}
return new DynamicMetaObject(target, restrictions);

嗯...虽然这仍然会返回null。理想情况下,我希望它与反射绑定器具有相同的行为-它知道它是null(如果将结果分配给变量,则会出错),但是如果您只是调用它,则不需要关心。无论如何+1,因为我不知道Expression.Empty :) - Jon Skeet
1
我同意 - 这里有些奇怪 - 但是由于 binder.ReturnTypeobject (而且它很挑剔)...(我确实开始说“我不喜欢这个”...) - Marc Gravell

6
也许调用者期望返回 null,但却丢弃了结果——这个枚举看起来很有趣,特别是 "ResultDiscarded" 标志...
[Flags, EditorBrowsable(EditorBrowsableState.Never)]
public enum CSharpBinderFlags
{
    BinaryOperationLogical = 8,
    CheckedContext = 1,
    ConvertArrayIndex = 0x20,
    ConvertExplicit = 0x10,
    InvokeSimpleName = 2,
    InvokeSpecialName = 4,
    None = 0,
    ResultDiscarded = 0x100,
    ResultIndexed = 0x40,
    ValueFromCompoundAssignment = 0x80
}

值得思考的问题...

更新:

从Microsoft / CSharp / RuntimeBinder / DynamicMetaObjectProviderDebugView中可以获取更多提示,它被用作调试器的可视化工具。方法TryEvalMethodVarArgs检查委托并创建一个绑定器,结果被丢弃(???)

 Type delegateType = Expression.GetDelegateType(list.ToArray());
    if (string.IsNullOrEmpty(name))
    {
        binder = new CSharpInvokeBinder(CSharpCallFlags.ResultDiscarded, AccessibilityContext, list2.ToArray());
    }
    else
    {
        binder = new CSharpInvokeMemberBinder(CSharpCallFlags.ResultDiscarded, name, AccessibilityContext, types, list2.ToArray());
    }
    CallSite site = CallSite.Create(delegateType, binder);

我在Reflector的使用上遇到了困难,但是这段代码的框架似乎有点奇怪,因为TryEvalMethodVarArgs方法本身期望一个对象作为返回类型,而最后一行却返回了动态调用的结果。我可能在错误的方向上努力了。

5
C#绑定程序(在Microsoft.CSharp.dll中)知道结果是否被使用;正如x0n (+1)所说,它在标志中跟踪它。不幸的是,该标志位于私有类型CSharpInvokeMemberBinder实例内部。
看起来C#绑定机制使用ICSharpInvokeOrInvokeMemberBinder.ResultDiscarded(内部接口上的属性)来读取它;CSharpInvokeMemberBinder实现了该接口(和属性)。工作似乎在Microsoft.CSharp.RuntimeBinder.BinderHelper.ConvertResult()中完成。如果表达式的类型为void且前面提到的ResultDiscarded属性未返回true,则该方法会抛出代码。
因此,在Beta 2中,我认为没有简单的方法可以解析出C#绑定程序中表达式结果被舍弃的事实。

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