2010-06-29 10 views
71

Gelegentlich verbringe ich gerne einige Zeit damit, den .NET-Code zu betrachten, nur um zu sehen, wie Dinge hinter den Kulissen implementiert werden. Ich bin über dieses Juwel gestolpert, während ich über Reflector die Methode String.Equals betrachtet habe.Warum überprüfen Sie dies! = Null?

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 
} 

Was ist der Grund für this gegen null Überprüfung? Ich muss annehmen, dass es einen Zweck gibt, sonst wäre dies wahrscheinlich schon erwischt und entfernt worden.

+1

Können Sie auch einen Blick auf EqualsHelper nehmen? Es scheint, dass sie EqualsHelper verwenden wollten, aber sie können die Nullwerte nicht so behandeln, wie sie es wollten. –

+0

Dies ist besonders interessant, da die Dokumentation explizit besagt, dass Equals eine NullReferenceException auslöst, wenn die Instanz null ist. – womp

+0

Meine Vermutung ist, dass es entweder ein Versehen ist oder etwas damit zu tun hat, wie 'EqualsHelper' funktioniert. Ich kann nicht wirklich eine Notwendigkeit für diese 'if' Aussage sehen, vorausgesetzt,' EqualsHelper' würde 'false' zurückgeben, wenn' strB' 'null' ist und' this' nicht. Aber vielleicht bin ich einfach nicht intelligent genug, um zu verstehen :) –

Antwort

85

Ich nehme an, Sie haben sich die .NET 3.5-Implementierung angesehen? Ich glaube, dass die Implementierung von .NET 4 etwas anders ist.

Ich habe jedoch einen schleichenden Verdacht, dass dies daran liegt, dass es möglich ist, auch virtuelle Instanz Methoden nicht virtuell auf eine Null-Referenz aufrufen. Möglich in IL, das ist. Ich werde sehen, ob ich etwas IL produzieren kann, das null.Equals(null) rufen würde.

EDIT: Okay, hier einige interessante Code:

.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 

ich diese bekam durch den folgenden C# -Code kompilieren:

using System; 

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

    } 
} 

... und dann mit ildasm und Bearbeitung zu zerlegen. Beachten Sie die folgende Zeile:

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

Ursprünglich war das callvirt statt call.

Also, was passiert, wenn wir es wieder zusammensetzen? Nun, mit .NET 4.0 bekommen wir das:

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

Hmm. Was ist mit .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() 

Nun, das ist interessanter ... wir haben deutlich bekommen in EqualsHelper verwaltet, was würden wir normalerweise nicht zu erwarten.

Genug der Zeichenfolge ... wollen wir versuchen Referenz Gleichheit selbst zu implementieren und sehen, ob wir null.Equals(null) true zurück bekommen kann:

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

Die gleiche Prozedur wie vor - zerlegen, callvirt-call ändern, wieder zusammenzusetzen, und beobachten sie es true drucken ...

Beachten sie, dass, obwohl eine andere Antworten verweist this C++ question, wir sind noch hier verschlagen ... weil wir den Aufruf einer virtuellen Methode nicht -virtuell. Normalerweise verwendet sogar der C++/CLI-Compiler callvirt für eine virtuelle Methode. Mit anderen Worten, ich denke, in diesem speziellen Fall ist der einzige Weg für this Null zu sein, die IL von Hand zu schreiben.


EDIT: Ich habe gerade etwas bemerkt ... ich eigentlich nicht in entweder unserer kleinen Beispielprogramme die richtige Methode aufrufen. Hier ist der Aufruf im ersten Fall:

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

hier ist der Aufruf in den zweiten:

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

Im ersten Fall, ich bedeuten System.String::Equals(object) zu nennen, und in den zweiten, ich gemeint anrufen Test::Equals(object). Von diesem können wir drei Dinge sehen:

  • Sie müssen vorsichtig mit Überladung sein.
  • Der C# -Compiler sendet Aufrufe an den -Deklarator der virtuellen Methode - nicht die spezifischste überschreiben der virtuellen Methode. IIRC, VB arbeitet die entgegengesetzte Richtung
  • object.Equals(object) ist glücklich, eine Null „diese“ Referenz

zu vergleichen Wenn Sie ein bisschen von Konsolenausgabe zur Überschreibung C# hinzufügen, können Sie den Unterschied sehen können - es wird nicht aufgerufen werden, es sei denn, Sie ändern die AWL so, dass sie explizit aufgerufen wird:

Also, da sind wir. Spaß und Missbrauch von Instanzmethoden auf Null-Referenzen.

Wenn Sie es bis hierher geschafft haben, können Sie auch meinen Blogpost über how value types can declare parameterless constructors ... in IL ansehen.

+8

/Ich schnappt mir Popcorn und warte auf die Show. – Greg

+0

Ich habe keine .NET 4 auf diesem Rechner. Es wäre interessant zu sehen, wie die Implementierung in dieser Version aussieht. Interessante Hypothese. Lassen Sie uns wissen, was Sie herausfinden. –

+1

Faszinierend. Also könnte ein Hardcore-IL-Programmierer eine API verwenden, die ich falsch entwickle, und die einzige Möglichkeit, vor der ich mich schützen kann, ist "this! = Null" bei jeder Instanzmethode zu überprüfen?Denken Sie nur an die Implikationen, wenn diese Bibliothek sicherheitsbezogen sein sollte! –

1

Mal sehen ... this ist die erste Zeichenfolge, die Sie vergleichen. obj ist das zweite Objekt. Es sieht also so aus, als ob es sich um eine Art Optimierung handelt. Es wird zuerst obj in einen String-Typ umgewandelt. Und wenn das fehlschlägt, ist strB null. Und wenn strB null ist, während this nicht ist, dann sind sie definitiv nicht gleich und die EqualsHelper Funktion kann übersprungen werden.

Das wird einen Funktionsaufruf speichern. Darüber hinaus könnte ein besseres Verständnis der EqualsHelper Funktion vielleicht ein wenig Licht in die Frage bringen, warum diese Optimierung benötigt wird.

EDIT:

Ah, so dass die Funktion eine EqualsHelper (string, string) als Parameter akzeptiert. Wenn strB Null ist, bedeutet das im Wesentlichen, dass es entweder ein Null-Objekt war, mit dem es beginnen konnte, oder es konnte nicht erfolgreich in eine Zeichenfolge umgewandelt werden. Wenn der Grund für strB Null ist, dass das Objekt ein anderer Typ war, der nicht in eine Zeichenfolge konvertiert werden konnte, dann würden Sie EqualsHelper nicht mit im Wesentlichen zwei Null-Werten aufrufen (die True zurückgeben). Die Equals-Funktion sollte in diesem Fall false zurückgeben. Diese if-Anweisung ist also mehr als eine Optimierung, sie stellt auch die korrekte Funktionalität sicher.

17

Der Grund dafür ist, dass es für this tatsächlich möglich ist, null zu sein.Es gibt zwei IL-Op-Codes, mit denen eine Funktion aufgerufen werden kann: call und callvirt. Die Callvirt-Funktion bewirkt, dass die CLR beim Aufrufen der Methode eine Nullprüfung durchführt. Die Aufrufanweisung erlaubt nicht und ermöglicht somit die Eingabe einer Methode mit this als null.

Sound gruselig? In der Tat ist es ein bisschen. Die meisten Compiler stellen jedoch sicher, dass dies nie passiert. Die Anweisung .call wird nur ausgegeben, wenn null keine Möglichkeit ist (ich bin ziemlich sicher, dass C# immer callvirt verwendet).

Dies trifft zwar nicht für alle Sprachen zu und aus Gründen, die ich nicht genau kenne, entschied sich das BCL-Team, die Klasse System.String in diesem Fall weiter zu härten.

Ein anderer Fall, in dem dies Popup ist, ist in umgekehrten pinvoke Anrufen.

9

Die kurze Antwort ist, dass Sprachen wie C# Sie zwingen, eine Instanz dieser Klasse vor dem Aufruf der Methode zu erstellen, aber das Framework selbst nicht. Es gibt zwei verschiedene Arten in CIL, um eine Funktion aufzurufen: call und callvirt .... Im Allgemeinen wird C# immer callvirt ausgeben, was erfordert, dass this nicht null ist. Aber andere Sprachen (C++/CLI kommt in den Sinn) könnte call ausgeben, die diese Erwartung nicht hat.

(¹okay, es ist mehr wie fünf, wenn Sie Kalli zählen, NEWOBJ etc, aber wir halten es einfach)

+0

Nein, C++ würde auch hier 'callvirt' ausgeben. Es ist schließlich eine virtuelle Methode. Siehe meine Antwort. –

0

Wenn das Argument (obj) gegossen nicht auf einen String, dann wird strB null sein und die Ergebnis sollte falsch sein. Beispiel:

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

schreibt false.

Denken Sie daran, dass die string.Equals() -Methode für jeden Argumenttyp aufgerufen wird, nicht nur für andere Strings.

+0

Das ist definitiv richtig. Das ist eigentlich ein Kesselblechcode, der für alle "Equals" -Implementierungen typisch ist. Das eigentliche Thema meiner Frage war, warum der Test 'this! = Null' gemacht wird. Die naive Behauptung ist, dass es überflüssig ist, aber mit einem unglaublich tiefen Wissen über die CLR und den C# -Compiler können Sie die Implementierung verstehen und wirklich schätzen. Siehe die akzeptierte Antwort für weitere Informationen! –

+0

Übrigens, willkommen bei Stackoverflow :) –

4

Die source code hat diesen Kommentar:

dies notwendig ist, um gegen Reverse-pinvokes und anderen Anrufern , die die callvirt Anweisung nicht verwenden

Verwandte Themen