Tengo dudas sobre estos dos aspectos;

El primero;

        Test test = new Test();

        result = test.DoWork(_param);

Segundo;

       result = new Test().DoWork(_param);

¿Qué sucede si no asignamos la instancia recién creada a una variable y llamamos directamente al método?

Veo alguna diferencia entre dos vías en el código IL.


Este a continuación es la salida IL del primer código c #

 IL_0000:  ldstr      "job "
 IL_0005:  stloc.0
 IL_0006:  newobj     instance void Works.Test::.ctor()
 IL_000b:  stloc.1
 IL_000c:  ldloc.1
 IL_000d:  ldloc.0
 IL_000e:  callvirt   instance string Works.Test::DoWork(string)
 IL_0013:  pop
 IL_0014:  ret

Y esta es la salida IL del segundo código c #

 IL_0000:  ldstr      "job "
 IL_0005:  stloc.0
 IL_0006:  newobj     instance void Works.Test::.ctor()
 IL_000b:  ldloc.0
 IL_000c:  call       instance string Works.Test::DoWork(string)
 IL_0011:  pop
 IL_0012:  ret

¿Podría informarme?

5
Emre Kantar 26 ene. 2016 a las 17:16

2 respuestas

La mejor respuesta

La pregunta es un poco difícil de encontrar aquí, pero creo que lo que estás preguntando es:

¿Por qué la asignación de la referencia recién creada a una variable hace que el compilador genere una llamada virtual, pero llamar al método directamente genera una llamada?

Eres muy observador para notar esta sutil diferencia.

Antes de llegar a su pregunta, respondamos algunas otras preguntas.

¿Debo confiar en que el compilador genera un buen código?

Generalmente sí. Hay errores ocasionales en la generación de código, pero este no es uno de ellos.

¿Es legal llamar a un método no virtual con callvirt?

Sí.

¿Es legal llamar a un método virtual con una llamada?

Sí, si está intentando, digamos, llamar a un método de clase base en lugar de anular una clase derivada. Pero normalmente esto no sucede.

¿El método que se llama en este ejemplo es virtual o no?

No es virtual.

Dado que el método no es virtual, se podría llamar con callvirt o call. ¿Por qué el compilador a veces genera callvirt y a veces genera llamada, cuando podría generar callvirt en ambas ocasiones o llamar en ambas ocasiones, de manera consistente?

Ahora llegamos a la parte interesante de tu pregunta.

Hay dos diferencias entre call y callvirt.

  • la llamada no hace envío virtual; callvirt busca el método correcto en la tabla de despacho de funciones virtuales antes de llamarlo. Por lo tanto, callvirt es aproximadamente un nanosegundo más lento.

  • callvirt siempre comprueba si el receptor es nulo , independientemente de si el método llamado es virtual o no. la llamada no comprueba si el receptor es nulo. Es legal llamar a un método con un "this" nulo mediante una llamada.

Quizás ahora veas a dónde va esto.

¿Se requiere que C # se bloquee con una excepción de desreferencia nula cada vez que se realiza una llamada en un receptor de referencia nulo?

. C # es obligatorio para bloquearse cuando invoca algo con un receptor nulo. Por lo tanto, C # tiene las siguientes opciones al generar código para llamar a un método:

  • Caso 1: Genere IL que verifique si es nulo, luego genere una llamada
  • Caso 2: Genere un callvirt.
  • Caso 3: Genere una llamada, pero no comience con una verificación nula.

El caso 1 es tonto. El IL es más grande, por lo que ocupa más espacio en el disco, es más lento de cargar, es más lento de mover. Sería una tontería generar este código cuando callvirt automáticamente hace una verificación nula.

El caso 2 es inteligente. El compilador de C # genera callvirts para que la verificación nula se realice automáticamente.

Ahora, ¿qué pasa con el caso 3? ¿En qué circunstancias puede C # omitir la verificación nula y generar una llamada? Sólo cuando:

  • La llamada es una llamada a un método no virtual y
  • C # ya sabe que el receptor no es nulo

Pero C # sabe que en new Foo().Bar() el receptor no puede ser nulo porque si lo fuera, entonces la construcción habría arrojado una excepción y nunca llegaríamos a la llamada.

El compilador no es lo suficientemente inteligente como para darse cuenta de que a la variable solo se le han asignado valores no nulos. Entonces genera un callvirt para estar seguro.

El compilador podría escribirse para ser así de inteligente. El compilador ya tiene que rastrear el estado de asignación de las variables para verificar la asignación definitiva. También podría rastrear el estado "se le asignó algo que podría ser nulo", y luego generaría una llamada en ambos casos. Pero el compilador no es (todavía) tan inteligente.

21
Eric Lippert 26 ene. 2016 a las 14:51

Si no hace nada más, no hay una diferencia visible entre las dos formas de hacerlo.

La única diferencia real es que en el primer caso, asigna su instancia Test a una variable, por lo que podrá acceder a ella más tarde / inspeccionarla en el depurador. No puede hacer eso en el segundo caso.

En términos de lógica, y siempre que no hagas nada más tarde con test, no habrá diferencia excepto una mejora de rendimiento realmente menor en el segundo caso (tan pequeña que no puedo pensar en ningún escenario real en el que podría contar).

0
Falanwe 26 ene. 2016 a las 14:21