Por qué verificar esto! = Nulo?

Ocasionalmente, me gusta pasar un tiempo mirando el código .NET solo para ver cómo se implementan las cosas detrás de escena. Me encontré con esta joya mientras miraba el método String.Equals través de Reflector.

DO#

 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] public override bool Equals(object obj) { string strB = obj as string; if ((strB == null) && (this != null)) { return false; } return EqualsHelper(this, strB); } 

ILLINOIS

 .method public hidebysig virtual instance bool Equals(object obj) cil managed { .custom instance void System.Runtime.ConstrainedExecution.ReliabilityContractAttribute::.ctor(valuetype System.Runtime.ConstrainedExecution.Consistency, valuetype System.Runtime.ConstrainedExecution.Cer) = { int32(3) int32(1) } .maxstack 2 .locals init ( [0] string str) L_0000: ldarg.1 L_0001: isinst string L_0006: stloc.0 L_0007: ldloc.0 L_0008: brtrue.s L_000f L_000a: ldarg.0 L_000b: brfalse.s L_000f L_000d: ldc.i4.0 L_000e: ret L_000f: ldarg.0 L_0010: ldloc.0 L_0011: call bool System.String::EqualsHelper(string, string) L_0016: ret } 

¿Cuál es el razonamiento para verificar this contra null ? Tengo que asumir que hay un propósito, de lo contrario, probablemente esto ya habría sido capturado y eliminado.

¿Supongo que estabas mirando la implementación de .NET 3.5? Creo que la implementación de .NET 4 es ligeramente diferente.

Sin embargo, tengo la sospecha de que esto se debe a que es posible llamar incluso a los métodos de instancia virtual de forma virtual en una referencia nula . Posible en IL, eso es. null.Equals(null) si puedo producir IL que llamaría null.Equals(null) .

EDITAR: Bien, aquí hay un código interesante:

 .method private hidebysig static void Main() cil managed { .entrypoint // Code size 17 (0x11) .maxstack 2 .locals init (string V_0) IL_0000: nop IL_0001: ldnull IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: ldnull IL_0005: call instance bool [mscorlib]System.String::Equals(string) IL_000a: call void [mscorlib]System.Console::WriteLine(bool) IL_000f: nop IL_0010: ret } // end of method Test::Main 

Lo obtuve comstackndo el siguiente código C #:

 using System; class Test { static void Main() { string x = null; Console.WriteLine(x.Equals(null)); } } 

… y luego desmontar con ildasm y edición. Tenga en cuenta esta línea:

 IL_0005: call instance bool [mscorlib]System.String::Equals(string) 

Originalmente, eso era callvirt lugar de call .

Entonces, ¿qué sucede cuando lo volvemos a armar? Bueno, con .NET 4.0 obtenemos esto:

 Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. at Test.Main() 

Hmm. ¿Qué pasa con .NET 2.0?

 Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. at System.String.EqualsHelper(String strA, String strB) at Test.Main() 

Ahora que es más interesante … claramente hemos logrado ingresar a EqualsHelper , lo que normalmente no hubiéramos esperado.

Suficiente de cadena … intentemos implementar la igualdad de referencia nosotros mismos, y veamos si podemos obtener null.Equals(null) para que sea verdadero:

 using System; class Test { static void Main() { Test x = null; Console.WriteLine(x.Equals(null)); } public override int GetHashCode() { return base.GetHashCode(); } public override bool Equals(object other) { return other == this; } } 

El mismo procedimiento que antes: desmontar, cambiar callvirt para call , volver a montar y verlo imprimir true

Tenga en cuenta que aunque hay otras respuestas que hacen referencia a esta pregunta en C ++ , estamos siendo aún más desviados aquí … porque estamos llamando a un método virtual de forma no virtual. Normalmente incluso el comstackdor C ++ / CLI usará callvirt para un método virtual. En otras palabras, creo que en este caso particular, la única forma de que this sea ​​nulo es escribiendo el IL a mano.


EDITAR: Acabo de notar algo … En realidad, no estaba llamando al método correcto en ninguno de nuestros pequeños progtwigs de muestra. Aquí está la llamada en el primer caso:

 IL_0005: call instance bool [mscorlib]System.String::Equals(string) 

aquí está la llamada en el segundo:

 IL_0005: call instance bool [mscorlib]System.Object::Equals(object) 

En el primer caso, quise llamar a System.String::Equals(object) , y en el segundo, quise llamar a Test::Equals(object) . De esto podemos ver tres cosas:

  • Debe tener cuidado con la sobrecarga.
  • El comstackdor de C # emite llamadas al declarante del método virtual, no la anulación más específica del método virtual. IIRC, VB funciona de manera opuesta
  • object.Equals(object) se complace en comparar una referencia “this” nula

Si agrega un poco de salida de la consola a la anulación de C #, puede ver la diferencia: no se ejecutará a menos que cambie la IL para llamarla de manera explícita, así:

 IL_0005: call instance bool Test::Equals(object) 

Entonces, ahí estamos. Diversión y abuso de métodos de instancia en referencias nulas.

Si has llegado hasta aquí, también te gustaría ver mi blog sobre cómo los tipos de valor pueden declarar constructores sin parámetros … en IL.

La razón es que, de hecho, es posible que this sea null . Hay 2 códigos operativos IL que se pueden usar para invocar una función: call y callvirt. La función callvirt hace que CLR realice una comprobación nula al invocar el método. La instrucción de llamada no permite y, por lo tanto, permite que se ingrese un método que es null .

Suena aterrador? De hecho, es un poco. Sin embargo, la mayoría de los comstackdores aseguran que esto no ocurra nunca. La instrucción .call solo se genera cuando null no es una posibilidad (estoy bastante seguro de que C # siempre usa callvirt).

Sin embargo, esto no es cierto para todos los idiomas y por razones que no sé exactamente que el equipo BCL eligió para fortalecer aún más la clase System.String en esta instancia.

Otro caso donde esto puede aparecer es en llamadas de búsqueda inversa inversa.

La respuesta corta es que los lenguajes como C # le obligan a crear una instancia de esta clase antes de llamar al método, pero el Framework en sí no lo hace. Hay dos maneras diferentes en CIL para llamar a una función: call y callvirt … En términos generales, C # siempre emitirá callvirt , que requiere que this no sea nulo. Pero otros lenguajes (C ++ / CLI viene a la mente) podrían emitir call , que no tienen esa expectativa.

(¹okay, es más como cinco si cuentas calli, newobj, etc., pero hagámoslo simple)

El código fuente tiene este comentario:

esto es necesario para protegerse contra golpes invertidos y otras llamadas que no usan la instrucción callvirt

Veamos … this es la primera cadena que estás comparando. obj es el segundo objeto. Entonces parece que es una optimización de géneros. Primero está lanzando obj a un tipo de cadena. Y si eso falla, entonces strB es nulo. Y si strB es nulo mientras que no es así, entonces definitivamente no son iguales y se puede omitir la función EqualsHelper .

Eso salvará una llamada de función. Más allá de eso, quizás una mejor comprensión de la función EqualsHelper pueda arrojar algo de luz sobre por qué se necesita esta optimización.

EDITAR:

Ah, entonces la función EqualsHelper está aceptando a (string, string) como parámetros. Si strB es nulo, significa que, o bien era un objeto nulo para empezar, o bien no se pudo convertir con éxito en una cadena. Si el motivo por el que strB es nulo es que el objeto era de un tipo diferente que no se podría convertir a una cadena, entonces no querría llamar a EqualsHelper con esencialmente dos valores nulos (que devolverán verdadero). La función Equals debería devolver false en este caso. Entonces, si esta statement es más que una optimización, también asegura una funcionalidad adecuada.

Si el argumento (obj) no se convierte en una cadena, strB será nulo y el resultado será falso. Ejemplo:

  int[] list = {1,2,3}; Console.WriteLine("a string".Equals(list)); 

escribe false

Recuerde que se llama al método string.Equals () para cualquier tipo de argumento, no solo para otras cadenas.