2012-04-18 7 views
12

digamos que tenemos siguiente ejemplo de código en C#:¿Por qué compilador de C# método de producción para llamar método BaseClass en IL

class BaseClass 
    { 
    public virtual void HelloWorld() 
    { 
     Console.WriteLine("Hello Tarik"); 
    } 
    } 

    class DerivedClass : BaseClass 
    { 
    public override void HelloWorld() 
    { 
     base.HelloWorld(); 
    } 
    } 

    class Program 
    { 
    static void Main(string[] args) 
    { 
     DerivedClass derived = new DerivedClass(); 
     derived.HelloWorld(); 
    } 
    } 

Cuando ildasmed el siguiente código:

.method private hidebysig static void Main(string[] args) cil managed 
{ 
    .entrypoint 
    // Code size  15 (0xf) 
    .maxstack 1 
    .locals init ([0] class EnumReflection.DerivedClass derived) 
    IL_0000: nop 
    IL_0001: newobj  instance void EnumReflection.DerivedClass::.ctor() 
    IL_0006: stloc.0 
    IL_0007: ldloc.0 
    IL_0008: callvirt instance void EnumReflection.BaseClass::HelloWorld() 
    IL_000d: nop 
    IL_000e: ret 
} // end of method Program::Main 

Sin embargo, csc .exe convertido derived.HelloWorld(); ->callvirt instance void EnumReflection.BaseClass::HelloWorld(). ¿Porqué es eso? No mencioné BaseClass en ningún lugar en el método Main.

Y también si se está llamando BaseClass::HelloWorld() entonces yo esperaría call en lugar de callvirt ya que parece llamada directa a BaseClass::HelloWorld() método.

+2

Simplemente lanzando esta idea por ahí, pero no sé - esto podría ser una optimización del compilador ya que su método simplemente llama a la implementación base. – payo

+0

@payo no; incluso si la anulación tiene alguna lógica única, el callvirt sería el mismo. – phoog

+0

@phoog esto no es lo mismo para todos los idiomas - Puedo ver cómo este es el caso para C# tho – payo

Respuesta

20

La llamada va a BaseClass :: HelloWorld porque BaseClass es la clase que define el método. La forma en que el despacho virtual funciona en C# es que se llama al método en la clase base, y el sistema de despacho virtual es responsable de garantizar que se llame a la anulación más derivada del método.

Esta respuesta de Eric Lippert de es muy informativo: https://stackoverflow.com/a/5308369/385844

Como es su serie blog sobre el tema: http://blogs.msdn.com/b/ericlippert/archive/tags/virtual+dispatch/

¿Tiene alguna idea de por qué esto se lleva a cabo de esta manera? ¿Qué pasaría si estuviera llamando directamente al método derivado de la clase ToString? De esta manera no tuve mucho sentido para mí a primera vista ...

Se implementa de esta manera porque el compilador no rastrea el tipo de objetos en tiempo de ejecución, solo el tipo de tiempo de compilación de sus referencias. Con el código que publicó, es fácil ver que la llamada irá a la implementación DerivedClass del método. Pero supongamos que la variable derived se ha inicializado como esto:

Derived derived = GetDerived(); 

Es posible que GetDerived() devuelve una instancia de StillMoreDerived. Si StillMoreDerived (o cualquier clase entre Derived y StillMoreDerived en la cadena de herencia) anula el método, entonces sería incorrecto llamar a la implementación Derived del método.

Encontrar todos los valores posibles que una variable puede contener a través del análisis estático es resolver el problema de detención. Con un ensamblado .NET, el problema es aún peor, porque un ensamblado puede no ser un programa completo. Por lo tanto, el número de casos en los que el compilador podría razonablemente probar que derived no contiene una referencia a un objeto derivado (o una referencia nula) sería pequeño.

¿Cuánto costaría agregar esta lógica para que pueda emitir una instrucción call en lugar de callvirt? Sin duda, el costo sería mucho mayor que el pequeño beneficio derivado.

+0

Tiene sentido para mí :) Gracias por la respuesta. – Tarik

+0

¿Eso significa, en otras palabras, que lo que realmente está viendo es el funcionamiento del polimorfismo? El IL se maneja de esta manera para que en tiempo de ejecución se llame al método correcto anulado? –

+0

@BradRem eso es exactamente lo que significa. – phoog

9

La manera de pensar sobre esto es que los métodos virtuales definen una "ranura" en la que puede poner un método en tiempo de ejecución. Cuando emitimos una instrucción callvirt, estamos diciendo "en tiempo de ejecución, fíjate en qué hay en esta ranura e invoca".

La ranura se identifica por la información del método sobre el tipo que declaró el método virtual, no el tipo que anulaciones ella.

Sería perfectamente legal emitir un callvirt al método derivado; el tiempo de ejecución se daría cuenta de que el método derivado es el mismo intervalo que el método base y el resultado sería exactamente el mismo. Pero nunca hay ningún motivo para hacer eso. Es más claro si identificamos la ranura identificando el tipo que declara esa ranura.

1

Tenga en cuenta que esto sucede incluso si declara DerivedClass como sealed.

C# utiliza el operador callvirt para llamar a cualquier método de instancia (virtual o no) para obtener automáticamente un cheque nulo en el objeto de referencia - para criar a un NullReferenceException en el punto que se llama un método. De lo contrario, el NullReferenceException solo se generará en el primer uso real de cualquier miembro de instancia de la clase dentro del método, lo que puede ser sorprendente. Si no se utiliza ningún miembro de la instancia, el método podría completarse exitosamente sin presentar la excepción.

También debe recordar que IL no se ejecuta directamente. Primero se compila según las instrucciones nativas del compilador JIT y realiza varias optimizaciones según si está depurando el proceso. Descubrí que el JIT x86 para CLR 2.0 incluía un método no virtual, pero llamaba al método virtual. También incluía Console.WriteLine.

Cuestiones relacionadas