2010-06-29 12 views
71

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 a través de Reflector.¿Por qué verificar esto? = Nulo?

C#

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

IL

.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 el control de this contra null? Tengo que asumir que hay un propósito, de lo contrario, probablemente esto ya habría sido capturado y eliminado.

+1

¿Puedes echar un vistazo a EqualsHelper también? Parece que querían usar EqualsHelper, pero es posible que no maneje los valores nulos de la forma que quisieran. –

+0

Esto es especialmente interesante, ya que la documentación establece explícitamente que Equals lanzará una NullReferenceException si la instancia es nula .... – womp

+0

Supongo que es un descuido o tiene algo que ver con el funcionamiento de 'EqualsHelper'. Realmente no puedo ver la necesidad de esa declaración 'if' en absoluto, suponiendo que' EqualsHelper' devolvería 'false' cuando' strB' es 'null' y' this' no. Pero tal vez no soy lo suficientemente inteligente como para entender :) –

Respuesta

85

Supongo que estabas viendo la implementación .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 no virtualmente en una referencia nula. Posible en IL, eso es. Veré si puedo producir algunos IL que llamarían al null.Equals(null).

EDIT: Bueno, 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 

Tengo esta compilando 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, era callvirt en lugar de call.

Entonces, ¿qué sucede cuando lo montamos de nuevo? 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 que hemos conseguido entrar en EqualsHelper, que no habríamos esperado normalmente.

Basta de cadena ... vamos a tratar de poner en práctica la igualdad referencia a nosotros mismos y ver si podemos conseguir null.Equals(null) a devolver true:

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 - desmonte, cambie callvirt-call, volver a montar, y verlo imprimir true ...

en cuenta que aunque otras respuestas Referencias de this C++ question, estamos siendo aún más tortuosa aquí ... porque estamos llamando a un método no virtual -virtualmente. Normalmente incluso el compilador 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 escribir el IL manualmente.


EDIT: acabo de cuenta de algo ... yo no estaba realmente llamando al método justo en cualquiera de nuestros programas de muestra pequeños. 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, decir llamar System.String::Equals(object), y en el segundo, que significaba para llamar al Test::Equals(object). De esto podemos ver tres cosas:

  • Debe tener cuidado con la sobrecarga.
  • El compilador de C# emite llamadas al declarante del método virtual - no es el más específico el que reemplaza el del método virtual. IIRC, VB funciona de la manera opuesta
  • object.Equals(object) es feliz para comparar un valor nulo "este" referencia

Si agrega un poco de salida de la consola a la C# anulación, se puede ver la diferencia - no lo hará ser llamado a menos que cambie el IL para llamarlo explícitamente, 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 ha llegado hasta aquí, también puede consultar mi publicación de blog sobre how value types can declare parameterless constructors ... en IL.

+8

/me toma algunas palomitas de maíz y espera el espectáculo. – Greg

+0

No tengo .NET 4 en esta máquina. Sería interesante ver cómo se ve la implementación en esa versión. Interesante hipótesis Déjenos saber lo que descubres. –

+1

Fascinante. Entonces un programador de IL hardcore podría usar una API que desarrolle incorrectamente y la única forma en que podría evitarlo es verificar 'this! = Null' en cada método de instancia.¡Solo piense en las implicaciones si dicha biblioteca estuviera relacionada con la seguridad! –

1

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

Eso guardará 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.

EDIT:

Ah, lo que la función EqualsHelper está aceptando un (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 podía convertir a una cadena, entonces no querría llamar a EqualsHelper con esencialmente dos valores nulos (que devolverá verdadero). La función Igual debe devolver falso en este caso. Entonces, si esta declaración es más que una optimización, también asegura una funcionalidad adecuada.

17

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 con this siendo null.

¿Da miedo? De hecho, es un poco. Sin embargo, la mayoría de los compiladores 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).

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

Otro caso en el que esto puede aparecer es en llamadas inversas pinvoke.

9

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 this para no ser nulo. Pero otros lenguajes (C++/CLI viene a la mente) podrían emitir call, que no tiene esa expectativa.

(¹okay, es más como cinco si contamos callos, newobj etc, pero vamos a mantenerlo simple)

+0

No, C++ también emitirá 'callvirt' aquí. Es un método virtual, después de todo. Ver mi respuesta –

0

Si el argumento (obj) no echa a una cadena continuación strB será nula y la el resultado debe 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.

+0

Eso es definitivamente cierto. Eso es en realidad el código de la placa de la caldera que es típico de todas las implementaciones de 'Igual'. La verdadera cuestión de mi pregunta fue por qué se hizo la prueba 'this! = Null'. La afirmación ingenua es que es superfluo, pero con un conocimiento increíblemente profundo del CLR y el compilador de C#, puede comprender y apreciar realmente la implementación. ¡Vea la respuesta aceptada para más información! –

+0

Por cierto, bienvenido a Stackoverflow :) –

4

El source code tiene este comentario:

esto es necesario para prevenir un retroceso-pinvokes y otras personas que llaman que no utilizan la instrucción callvirt

Cuestiones relacionadas