2010-06-18 22 views
33

Tengo las siguientes interfaces que son parte de un proyecto existente. Me gustaría hacer posible llamar a la función Store (...) con objetos dinámicos. Pero no quiero cambiar la jerarquía de la interfaz (si es posible).Comportamiento extraño al usar tipos dinámicos como parámetros de método

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()); 
    }  
} 

y el siguiente código:

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); 

Cuando llamo extendedInterfaceTest.Store(employee), se plantea una excepción de tiempo de ejecución de aglutinante. ¿Por qué el tipo de interfaz hace la diferencia cuando es el mismo tipo subyacente? Puedo llamarlo al IActualInterface y Type, pero no a IExtendedInterface?

Entiendo que al llamar a una función con un parámetro dinámico, la resolución ocurre en tiempo de ejecución, pero ¿por qué los diferentes comportamientos?

+1

Encontré esto trabajando en Raven ¿no? –

+1

@Chris seguro !! –

Respuesta

81

Lo que debe recordar es que la resolución dinámica básicamente realiza el mismo proceso que la resolución estática, pero en tiempo de ejecución. Cualquier cosa que no pueda ser resuelta por el CLR no será resuelta por el DLR.

Tomemos este pequeño programa, inspirado en la suya, y que no utiliza en absoluto dinámico:

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(); 
     } 
    } 
} 

Todo compila bien. Pero, ¿qué generó realmente el compilador? Veamos el uso de ILdasm.

Para las interfaces:

// 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) 

Podemos ver aquí que el compilador de C# siempre genera peticiones de la interfaz o clase en la que se define el método. IActualInterface tiene una ranura de método para Tienda por lo que se usa para actualTest.Store. IExtendedInterface no, por lo que IActualInterface se usa para la llamada. TestInterface define un nuevo método Almacenar, utilizando el modificador newslot IL, asignando efectivamente un nuevo intervalo en el vtable para ese método, por lo que se usa directamente ya que directTest es del tipo TestInterface.

Para las clases:

// 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) 

Para los 3 tipos diferentes, se genera la misma llamada porque la ranura método se define en ActualClass.

Veamos ahora qué obtenemos si escribimos la IL nosotros mismos, usando el tipo que queremos en lugar de dejar que el compilador de C# lo elija por nosotros. He modificado la IL a tener este aspecto:

Para las interfaces:

// 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) 

Para las clases:

// 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) 

El programa compila bien con ILASM. Sin embargo, no logra pasar a PEverify y se estrella en tiempo de ejecución con el error siguiente:

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

Si se quita esta llamada no válida, las clases de llamadas derivadas funcionan bien sin ningún error. El CLR puede resolver el método base desde la llamada de tipo derivada.Sin embargo, las interfaces no tienen representación verdadera en el tiempo de ejecución, y el CLR no puede resolver la llamada al método desde la interfaz extendida.

En teoría, el compilador de C# podría emitir la llamada directamente a la clase correcta especificada en el tiempo de ejecución. Evitaría problemas sobre llamadas de clase media como se ve en Eric Lippert's blog. Sin embargo, como se demostró, esto no es posible para las interfaces.

Volvamos al DLR. Resuelve el método exactamente de la misma manera que el CLR. Hemos visto que el CLR no pudo resolver IExtendedInterface.Store. ¡El DLR tampoco! Esto está totalmente oculto por el hecho de que el compilador de C# emitirá la llamada correcta, por lo que siempre tenga cuidado al usar dynamic a menos que sepa perfectamente cómo funciona en el CLR.

+0

¡Gracias por la respuesta en profundidad! –

+0

+1 para una respuesta excelente con votos por votos insuficientes. :) –

Cuestiones relacionadas