【问题标题】:Strange behaviour when using dynamic types as method parameters使用动态类型作为方法参数时的奇怪行为
【发布时间】:2011-03-05 12:55:41
【问题描述】:

我有以下接口,它们是现有项目的一部分。我想让使用动态对象调用 Store(..) 函数成为可能。但我不想更改接口层次结构(如果可能的话)。

public interface IActualInterface
{
    void Store(object entity);    
}
public interface IExtendedInterface : IActualInterface
{
    //Interface items not important
}        
public class Test : IExtendedInterface 
{
    public void Store(object entity)
    {
        Console.WriteLine("Storing: " + entity.ToString());
    }       
}

以及以下代码:

IExtendedInterface extendedInterfaceTest = new Test();
IActualInterface actualInterfaceTest = new Test();
Test directTest = new Test();

dynamic employee = new ExpandoObject();
employee.Name = "John Smith";
employee.Age = 33;
employee.Phones = new ExpandoObject();
employee.Phones.Home = "0111 123123";
employee.Phones.Office = "027 321123";
employee.Tags = new List<dynamic>() { 123.4D, 99.54D };

try
{
    extendedInterfaceTest .Store(employee);
}
catch (RuntimeBinderException rbEx)
{
    Console.WriteLine(rbEx.Message);
}

//Casting as (object) works okay as it's not resolved at runtime
extendedInterfaceTest.Store((object)employee);

//this works because IActualInterface implements 'Store'
actualInterfaceTest.Store(employee);
//this also works okay (directTest : IProxyTest)
directTest.Store(employee);

当我调用extendedInterfaceTest.Store(employee) 时,它会引发运行时绑定程序异常。当接口类型是相同的底层类型时,为什么接口类型会有所不同?我可以在IActualInterfaceType 上调用它,但不是IExtendedInterface

我知道当调用带有动态参数的函数时,解析发生在运行时,但为什么会有不同的行为?

【问题讨论】:

  • 在 Raven 上找到了这个工作,不是吗?

标签: dynamic c#-4.0


【解决方案1】:

您需要记住的是,动态解析基本上与静态解析执行相同的过程,但在运行时。 CLR 无法解决的任何问题都不会由 DLR 解决。

让我们来看看这个受你启发的小程序,它根本不使用动态:

namespace ConsoleApplication38 {

    public interface IActualInterface {
        void Store(object entity);
    }
    public interface IExtendedInterface : IActualInterface {
    }
    public class TestInterface : IExtendedInterface {
        public void Store(object entity) {
        }
    }

    public abstract class ActualClass {
        public abstract void Store(object entity);
    }
    public abstract class ExtendedClass : ActualClass { 
    }
    public class TestClass : ExtendedClass {
        public override void Store(object entity) {
        }
    }

    class Program {

        static void TestInterfaces() {
            IActualInterface actualTest = new TestInterface();
            IExtendedInterface extendedTest = new TestInterface();
            TestInterface directTest = new TestInterface();
            
            actualTest.Store(null);
            extendedTest.Store(null);
            directTest.Store(null);
        }

        static void TestClasses() {
            ActualClass actualTest = new TestClass();
            ExtendedClass extendedTest = new TestClass();
            TestClass directTest = new TestClass();

            actualTest.Store(null);
            extendedTest.Store(null);
            directTest.Store(null);
        }

        static void Main(string[] args) {
            TestInterfaces();
            TestClasses();
        }
    }
}

一切都编译得很好。但是编译器真正生成了什么?让我们看看如何使用 ILdasm。

对于接口:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestInterface::Store(object)

我们可以在这里看到,C# 编译器总是为定义方法的接口或类生成调用。 IActualInterface 有一个用于 Store 的方法槽,因此它用于actualTest.StoreIExtendedInterface 没有,所以IActualInterface 用于调用。 TestInterface 定义了一个新方法 Store,使用 newslot IL 修饰符,有效地在 vtable 中为该方法分配了一个新槽,因此直接使用它,因为 directTestTestInterface 类型。

对于课程:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

对于 3 种不同的类型,由于方法槽是在 ActualClass 上定义的,因此会生成相同的调用。

现在让我们看看如果我们自己编写 IL,使用我们想要的类型而不是让 C# 编译器为我们选择它,我们会得到什么。我已将 IL 修改为如下所示:

对于接口:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.IExtendedInterface::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestInterface::Store(object)

对于课程:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.ExtendedClass::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestClass::Store(object)

该程序使用 ILasm 编译得很好。但是它无法通过 peverify 并在运行时崩溃并出现以下错误:

未处理的异常: System.MissingMethodException:方法 未找到:'无效 ConsoleApplication38.IExtendedInterface.Store(System.Object)'。 在 ConsoleApplication38.Program.TestInterfaces() 在 ConsoleApplication38.Program.Main(字符串 [] 参数)

如果您删除此无效调用,派生类调用可以正常工作而不会出现任何错误。 CLR 能够从派生类型调用中解析基方法。但是接口在运行时没有真正的表示,CLR 无法解析来自扩展接口的方法调用。

理论上,C# 编译器可以直接向运行时中指定的正确类发出调用。它将避免Eric Lippert's blog 上看到的有关中产阶级呼叫的问题。但是,正如演示的那样,这对于接口是不可能的。

让我们回到 DLR。它以与 CLR 完全相同的方式解析该方法。我们已经看到 CLR 无法解析 IExtendedInterface.Store。 DLR 也不能! C# 编译器会发出正确的调用这一事实完全隐藏了这一点,因此在使用 dynamic 时请务必小心,除非您完全了解它在 CLR 中的工作原理。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-22
  • 1970-01-01
  • 2012-01-17
  • 1970-01-01
  • 1970-01-01
  • 2020-09-13
相关资源
最近更新 更多