Warum das überprüfen! = Null?

72

Gelegentlich verbringe ich gerne einige Zeit damit, mir den .NET-Code anzusehen, um zu sehen, wie die Dinge hinter den Kulissen implementiert werden. Ich bin auf dieses Juwel gestoßen, als ich mir die String.EqualsMethode über Reflector angesehen habe.

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 die Überprüfung thisvor null? Ich muss davon ausgehen, dass es einen Zweck gibt, sonst wäre dies wahrscheinlich inzwischen gefangen und entfernt worden.

Brian Gideon
quelle
1
Können Sie sich auch EqualsHelper ansehen? Es scheint, dass sie EqualsHelper verwenden wollten, aber es kann sein, dass die Nullwerte nicht so behandelt werden, wie sie es wollten.
Danny T.
Dies ist besonders interessant, da in der Dokumentation ausdrücklich angegeben ist, dass Equals eine NullReferenceException auslöst, wenn die Instanz null ist ....
womp
Ich vermute, es ist entweder ein Versehen oder hat etwas damit zu tun, wie es EqualsHelperfunktioniert. Ich kann nicht wirklich sehen, dass diese ifAussage überhaupt notwendig ist , vorausgesetzt, sie EqualsHelperwürde zurückkehren, falsewenn sie strBist nullund thisnicht. Aber vielleicht bin ich einfach nicht klug genug zu verstehen :)
Nate Pinchot
@womp - in 4.0 ist die erste Zeile if(this == null) throw new NullReferenceException()so, dass sie in diesem Sinne korrekt ist.
John Rasch
11
Dieser Code wurde vor dem 13. Dezember 1999 geschrieben, dem Tag, an dem sich das C # -Team zur Implementierung von Nullprüfungen verpflichtet hat. blogs.msdn.com/b/ericgu/archive/2008/07/02/…
Hans Passant

Antworten:

85

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

Ich habe jedoch den Verdacht, dass dies daran liegt, dass sogar virtuelle Instanzmethoden nicht virtuell für eine Nullreferenz aufgerufen werden können . Möglich in IL, das heißt. Ich werde sehen, ob ich IL produzieren kann, die anrufen würde null.Equals(null).

EDIT: Okay, hier ist ein interessanter 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 habe dies durch Kompilieren des folgenden C # -Codes erhalten:

using System;

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

    }
}

... und dann zerlegen ildasmund bearbeiten. Beachten Sie diese Zeile:

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

Ursprünglich war das callvirtstatt call.

Was passiert also, wenn wir es wieder zusammenbauen? Nun, mit .NET 4.0 bekommen wir Folgendes:

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()

Das ist interessanter ... wir haben es eindeutig geschafft, uns darauf einzulassen EqualsHelper, was wir normalerweise nicht erwartet hätten.

Genug der Zeichenfolge ... Lassen Sie uns versuchen, die Referenzgleichheit selbst zu implementieren und zu prüfen, ob wir null.Equals(null)true zurückgeben können:

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 - auseinanderbauen, ändern callvirtzu call, wieder zusammenbauen, und beobachten Sie es drucken true...

Beachten Sie, dass, obwohl eine andere Antwort auf diese C ++ - Frage verweist , wir hier noch hinterhältiger sind ... weil wir eine virtuelle Methode nicht virtuell aufrufen . Normalerweise wird sogar der C ++ / CLI-Compiler callvirtfür eine virtuelle Methode verwendet. Mit anderen Worten, ich denke in diesem speziellen Fall besteht die einzige Möglichkeit this, Null zu sein, darin, die IL von Hand zu schreiben.


EDIT: Ich habe gerade etwas bemerkt ... Ich habe in keinem unserer kleinen Beispielprogramme die richtige Methode aufgerufen. Hier ist der Anruf im ersten Fall:

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

Hier ist der Anruf im zweiten:

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

Im ersten Fall, ich meinte zu Anruf System.String::Equals(object), und in der zweiten, ich soll anrufen Test::Equals(object). Daraus können wir drei Dinge erkennen:

  • Sie müssen mit Überlastung vorsichtig sein.
  • Der C # -Compiler sendet Aufrufe an den Deklarator der virtuellen Methode - nicht die spezifischste Überschreibung der virtuellen Methode. IIRC, VB arbeitet umgekehrt
  • object.Equals(object) freut sich, eine Null "diese" Referenz zu vergleichen

Wenn Sie der C # -Überschreibung ein wenig Konsolenausgabe hinzufügen, können Sie den Unterschied erkennen. Sie wird nur aufgerufen, wenn Sie die IL so ändern, dass sie explizit aufgerufen wird:

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

Da sind wir also. Spaß und Missbrauch von Instanzmethoden bei Nullreferenzen.

Wenn Sie es bis hierher geschafft haben, möchten Sie vielleicht auch meinen Blog-Beitrag darüber lesen, wie Werttypen parameterlose Konstruktoren deklarieren können ... in IL.

Jon Skeet
quelle
Ich habe kein .NET 4 auf diesem Computer. Es wäre interessant zu sehen, wie die Implementierung in dieser Version aussieht. Interessante Hypothese. Lassen Sie uns wissen, was Sie herausfinden.
Brian Gideon
1
Faszinierend. Ein Hardcore-IL-Programmierer könnte also eine API verwenden, die ich falsch entwickelt habe, und die einzige Möglichkeit, mich dagegen zu schützen, besteht darin, this != nulljede Instanzmethode zu überprüfen . Denken Sie nur an die Auswirkungen, wenn diese Bibliothek sicherheitsrelevant sein sollte!
Brian Gideon
12
Freakin 'Jon Skeet. Ich wusste, dass du in allem verrückt und gut bist, aber das ist mehr als lächerlich erstaunlich. Ich würde dir meinen Hut abnehmen, aber ich trage keinen. Hier ist stattdessen eine Gegenstimme, du frecher No-Goodnik du.
Platinum Azure
1
@Brian, Ich vermute, dass der Jitter in .NET4 eine Prüfung auf Nicht-Null-Arg0 für den Aufruf hinzufügt, während .NET2 diese Prüfung nur für Callvirt durchführte (wo dies definitiv notwendig war, da es an der virtuellen Methodentabelle abgerufen werden musste. )
Dan Bryant
@Dan: Nein, ich glaube, die Überprüfung auf Nicht-Nichtigkeit wird in IL im .NET 4-Code durchgeführt, und der einzige Grund, warum wir dies nicht sehen, ist Inlining.
Jon Skeet
17

Der Grund dafür ist, dass es tatsächlich möglich ist thiszu sein null. Es gibt 2 IL-Operationscodes, mit denen eine Funktion aufgerufen werden kann: call und callvirt. Die Funktion callvirt bewirkt, dass die CLR beim Aufrufen der Methode eine Nullprüfung durchführt. Die Aufrufanweisung erlaubt es nicht und ermöglicht daher, eine Methode mit thisSein einzugeben null.

Klingt beängstigend? In der Tat ist es ein bisschen. Die meisten Compiler stellen jedoch sicher, dass dies niemals geschieht. Die .call-Anweisung wird immer nur ausgegeben, wenn dies nullnicht möglich ist (ich bin mir ziemlich sicher, dass C # immer callvirt verwendet).

Dies gilt jedoch nicht für alle Sprachen, und aus Gründen, die ich nicht genau kenne, hat sich das BCL-Team entschieden, die System.StringKlasse in diesem Fall weiter zu verbessern .

Ein weiterer Fall, in dem dies auftreten kann, sind umgekehrte Pinvoke-Aufrufe.

JaredPar
quelle
9

Die kurze Antwort lautet, dass Sprachen wie C # Sie zwingen, eine Instanz dieser Klasse zu erstellen, bevor Sie die Methode aufrufen, das Framework selbst jedoch nicht. In CIL gibt es zwei verschiedene Möglichkeiten, eine Funktion aufzurufen: callund callvirt... Im Allgemeinen wird C # immer ausgegeben callvirt, was erfordert this, dass es nicht null ist. Aber andere Sprachen (C ++ / CLI callfällt mir ein) könnten emittieren , was diese Erwartung nicht hat.

(¹okay, es ist eher fünf, wenn Sie Calli, Newobj usw. zählen, aber lassen Sie es uns einfach halten)

Warren Rumak
quelle
Nein, C ++ würde auch callvirthier emittieren . Immerhin ist es eine virtuelle Methode. Siehe meine Antwort.
Jon Skeet
4

Der Quellcode hat diesen Kommentar:

Dies ist erforderlich, um sich vor Reverse-Pinvokes und anderen Anrufern zu schützen, die die Callvirt-Anweisung nicht verwenden

Evgeniy Berezovsky
quelle
1

Mal sehen ... thisist die erste Zeichenfolge, die Sie vergleichen. objist das zweite Objekt. Es sieht also so aus, als wäre es eine Art Optimierung. Es wird zuerst objin einen Stringtyp umgewandelt. Und wenn das fehlschlägt, strBist null. Und wenn strBnull thisist, während nicht, dann sind sie definitiv nicht gleich und die EqualsHelperFunktion kann übersprungen werden.

Das spart einen Funktionsaufruf. Darüber hinaus EqualsHelperkönnte ein besseres Verständnis der Funktion Aufschluss darüber geben, warum diese Optimierung erforderlich ist.

BEARBEITEN:

Ah, also akzeptiert die EqualsHelper-Funktion a (string, string)als Parameter. Wenn strBnull ist, bedeutet dies im Wesentlichen, dass es entweder zunächst ein Nullobjekt war oder nicht erfolgreich in eine Zeichenfolge umgewandelt werden konnte. Wenn der Grund für strBdie Null ist, dass das Objekt ein anderer Typ war, der nicht in eine Zeichenfolge konvertiert werden konnte, möchten Sie EqualsHelper nicht mit im Wesentlichen zwei Nullwerten 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 ordnungsgemäße Funktionalität sicher.

Steve Wortham
quelle
0

Wenn das Argument (obj) nicht in einen String umgewandelt wird, ist strB null und das Ergebnis sollte falsch sein. Beispiel:

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

schreibt false.

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

John Alexiou
quelle
Das ist definitiv wahr. Das ist eigentlich der Kesselplattencode, der für alle EqualsImplementierungen typisch ist . Das eigentliche Fleisch meiner Frage war, warum der Test this != nullgemacht 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. Weitere Informationen finden Sie in der akzeptierten Antwort!
Brian Gideon