2009-05-10 15 views
27

Tengo curiosidad por saber por qué sucede esto. Por favor, lee el ejemplo de código siguiente y la IL correspondiente que se emite en los comentarios debajo de cada sección:¿Por qué el compilador C# emite una instrucción callvirt para una llamada al método GetType()?

using System; 

class Program 
{ 
    static void Main() 
    { 
     Object o = new Object(); 
     o.GetType(); 

     // L_0001: newobj instance void [mscorlib]System.Object::.ctor() 
     // L_0006: stloc.0 
     // L_0007: ldloc.0 
     // L_0008: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType() 

     new Object().GetType(); 

     // L_000e: newobj instance void [mscorlib]System.Object::.ctor() 
     // L_0013: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType() 
    } 
} 

¿Por qué el compilador emitirá un callvirt por primera sección, pero un call para la segunda sección? ¿Hay alguna razón para que el compilador emita alguna vez una instrucción callvirt para un método no virtual? Y si hay casos en los que el compilador emitirá un callvirt para un método no virtual, ¿esto crea problemas para la seguridad de tipo?

+1

muy buena pregunta ... me hizo Fuera del alcance de mis libros. – Gishu

+15

En resumen: Caso (1) invocar método virtual: generar callvirt. Caso (2) invocar el método de instancia en el receptor que acepta nulos: generar callvirt para obtener un cheque nulo barato - sí, esto es seguro. Caso (3) invoque el método de instancia en un receptor conocido que no admite nulos: generar llamada a _avoid_ null check. Su primer ejemplo cae en la categoría (2), su segundo ejemplo cae en la categoría (3). (El compilador sabe que lo nuevo nunca devuelve nulo y, por lo tanto, no es necesario volver a verificar). –

+0

Gracias Eric, la comprobación nula tiene mucho sentido. ¡Tu comentario más la respuesta de Gishu hacen las cosas mucho más claras! :) –

Respuesta

20

Simplemente jugando seguro.

Técnicamente C# compilador no siempre uso callvirt

Para los métodos estáticos & métodos definidos en los tipos de valor, se utiliza call. La mayoría se proporciona a través de la instrucción callvirt IL.

La diferencia que hizo girar el voto entre los dos es el hecho de que call asume que el "objeto que se utiliza para hacer la llamada" no es nulo. callvirt, por otro lado, comprueba si no es nulo y arroja una NullReferenceException si es necesario.

  • Para los métodos estáticos, el objeto es un tipo de objeto y no puede ser nulo. Lo mismo para los tipos de valor. Por lo tanto, se usa call para un mejor rendimiento.
  • Para los demás, los diseñadores de lenguaje decidieron ir con callvirt para que el compilador JIT verifique que el objeto que se utiliza para realizar la llamada no sea nulo. Incluso para los métodos de instancias no virtuales ... valoraban la seguridad sobre el rendimiento.

Ver también: Jeff Richter hace un mejor trabajo en esto - en el capítulo su 'Diseño de tipos' en CLR a través de C# 2ª Ed

+0

Creo que ambos usan llamada con el depurador desactivado [(el primero es callvirt en depuración porque puede colocar un punto de interrupción en la segunda línea y cambiar el valor de o por nulo)] (http://stackoverflow.com/a/193955/2850543). –

1

El compilador no conoce el tipo real de o en la primera expresión, pero sí conoce el tipo real en la segunda expresión. Parece que solo está mirando una declaración a la vez.

Esto está bien, porque C# depende en gran medida del JIT para la optimización. Es muy probable que en un caso tan simple las dos llamadas se conviertan en llamadas de instancia en tiempo de ejecución.

No creo que callvirt se haya emitido para métodos no virtuales, pero incluso si lo fuera, no habría problema porque el método nunca se anularía (por razones obvias).

+0

Eso solo explica callvirt para métodos virtuales. Pero GetType no es virtual. Es una función externa implementada en algún lugar profundo de las entrañas de la CLR (probablemente devolviendo un campo que está almacenado en el vtable del objeto o algo así). Es el mismo método para cada objeto. – Niki

+0

Lo suficientemente justo: supuse que GetType era virtual. Mi error. Me gusta la respuesta de Dustin. – zildjohn01

0

Me atrevo a adivinar que es porque el primero se asigna a una variable, que potencialmente podría contener una instancia descifrada de otro tipo que podría haber anulado GetType (aunque podemos ver que no); el segundo nunca podría ser otra cosa que Object.

27

Ver this publicación anterior del blog por Eric Gunnerson.

Aquí está el texto del mensaje:

¿Por qué C# siempre uso callvirt?

Esta pregunta surgió en un alias interno de C#, y pensé que la respuesta sería de interés general. Eso es suponiendo que la respuesta es correcta, ha pasado bastante tiempo.

El idioma .NET IL proporciona una instrucción call y callvirt, y callvirt se usa para llamar funciones virtuales. Pero si mira a través del código que genera C#, verá que genera un "callvirt" incluso en los casos en que no hay una función virtual involucrada. ¿Porque hace eso?

Volví a leer las notas de diseño del lenguaje que tengo, y dicen claramente que decidimos usar callvirt el 13/12/1999. Desafortunadamente, no captan nuestra lógica para hacerlo, así que tendré que alejarme de mi memoria.

Obtuvimos un informe de alguien (probablemente uno de los grupos .NET que usaban C# (pensó que todavía no se llamaba C# en ese momento)) que había escrito un código que llamaba un método en un puntero nulo, pero no obtuvo una excepción porque el método no accedía a ningún campo (es decir, "esto" era nulo, pero no había nada en el método utilizado). Ese método llamó a otro método que usó el punto y arrojó una excepción, y se produjo un poco de arañazo en la cabeza. Después de que lo descubrieron, nos enviaron una nota al respecto.

Pensamos que ser capaz de invocar un método en una instancia nula era un poco extraño. Peter Golde hizo algunas pruebas para ver cuál era el impacto de usar siempre callvirt, y fue lo suficientemente pequeño como para que decidiéramos hacer el cambio.

+3

Lástima que no hay forma de que un método no virtual pueda especificar que se debería invocar sin 'callvirt', ya que hubiera sido útil permitir que métodos como' string.IsNullOrEmpty' se pudieran usar en cadenas nulas. – supercat

+0

@supercat una forma de evitar esto es usar métodos de extensión, incluso si el objeto era nulo, aún se pasaría en el primer parámetro de extensión. – vexe

+0

Desafortunadamente no puede usar métodos de extensión con tipos genéricos en una función genérica para crear una decisión de tiempo de ejecución sobre (efectivamente) un método estático. – NetMage

3

Como (quizás-) aparte interesante ... GetType() es inusual porque no esvirtual - esto lleva a algunos very, very odd things.

(marcado como wiki, ya que es un poco fuera de tema a la pregunta real)

Cuestiones relacionadas