代码之家  ›  专栏  ›  技术社区  ›  Matt Warren

使用动态类型作为方法参数时的奇怪行为

  •  37
  • Matt Warren  · 技术社区  · 15 年前

    我有以下接口,它们是现有项目的一部分。我想让调用带有动态对象的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) ,它引发运行时绑定器异常。当接口类型是相同的基础类型时,为什么它会产生差异?我可以接通它 IActualInterface Type ,但不是 IExtendedInterface ?

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

    1 回复  |  直到 15 年前
        1
  •  86
  •   Julien Lebosquain    15 年前

    您需要记住的是,动态分辨率基本上与静态分辨率执行相同的过程,但在运行时执行。任何无法由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 有存储方法槽,因此用于 actualTest.Store . IExtendedInterface 不,所以 IActualInterface(IActualInterface) 用于呼叫。 TestInterface 使用定义新方法存储 newslot IL修饰符,有效地为该方法在vtable中分配了一个新的槽,因此它直接用于 directTest 属于类型 测试接口 .

    对于课程:

    // 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.iextendInterface.store(system.object)'. 在 控制台应用程序38.program.testinterfaces()。 在 consoleapplication38.program.main(字符串[] ARGS)

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

    理论上,C编译器可以直接发出对运行时指定的正确类的调用。它可以避免出现关于中产阶级电话的问题。 Eric Lippert's blog . 然而,正如所演示的,这对于接口来说是不可能的。

    让我们回到DLR。它以与clr完全相同的方式解析方法。我们已经看到了 IExtendedInterface.Store 无法由clr解析。DLR也不能!C编译器将发出正确的调用这一事实完全隐藏了这一点,因此在使用 dynamic 除非你完全知道它在CLR中是如何工作的。