2010-06-18 63 views
33

我有以下接口,它们是现有项目的一部分。我想使用动态对象调用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

我明白,当用动态参数调用一个函数时,解析发生在运行时,但为什么不同的行为?

+1

发现这个工作在乌鸦不是吗? – 2011-08-12 18:55:43

+1

@Chris确实没有! – 2011-08-22 19:10:08

回答

81

你需要记住的是动态分辨率基本上和静态分辨率一样,但是在运行时。任何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.StoreIExtendedInterface没有,所以IActualInterface用于呼叫。 TestInterface定义了一种新的方法Store,使用IL修改器,在该表的vtable中为该方法有效地分配了一个新插槽,因此directTest的类型为TestInterface,因此它可以直接使用。

对于类:

// 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和崩溃传:

Unhandled Exception: System.MissingMethodException: Method not found: 'Void ConsoleApplication38.IExtendedInterface.Store(System.Object)'. at ConsoleApplication38.Program.TestInterfaces() at ConsoleApplication38.Program.Main(String[] args)

如果删除该非法呼叫,派生类的调用没有任何错误做工精细。 CLR能够从派生类型调用中解析基本方法。但是,接口在运行时没有真实的表示,并且CLR无法从扩展接口解析方法调用。

理论上,C#编译器可以将调用直接发送到运行时指定的正确类。这样可以避免在Eric Lippert's blog上看到的有关中间电话的问题。然而如所证明的,这对于接口是不可能的。

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

+0

感谢您的深入解答! – 2010-06-18 20:52:19

+0

+1,对于没有足够的upvotes的很好的答案。 :) – 2010-09-13 12:46:59