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.Equals
Methode ü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 this
vor null
? Ich muss davon ausgehen, dass es einen Zweck gibt, sonst wäre dies wahrscheinlich inzwischen gefangen und entfernt worden.
EqualsHelper
funktioniert. Ich kann nicht wirklich sehen, dass dieseif
Aussage überhaupt notwendig ist , vorausgesetzt, sieEqualsHelper
würde zurückkehren,false
wenn siestrB
istnull
undthis
nicht. Aber vielleicht bin ich einfach nicht klug genug zu verstehen :)if(this == null) throw new NullReferenceException()
so, dass sie in diesem Sinne korrekt ist.Antworten:
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
ildasm
und bearbeiten. Beachten Sie diese Zeile:IL_0005: call instance bool [mscorlib]System.String::Equals(string)
Ursprünglich war das
callvirt
stattcall
.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
callvirt
zucall
, wieder zusammenbauen, und beobachten Sie es druckentrue
...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
callvirt
für eine virtuelle Methode verwendet. Mit anderen Worten, ich denke in diesem speziellen Fall besteht die einzige Möglichkeitthis
, 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 anrufenTest::Equals(object)
. Daraus können wir drei Dinge erkennen:object.Equals(object)
freut sich, eine Null "diese" Referenz zu vergleichenWenn 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.
quelle
this != null
jede Instanzmethode zu überprüfen . Denken Sie nur an die Auswirkungen, wenn diese Bibliothek sicherheitsrelevant sein sollte!Der Grund dafür ist, dass es tatsächlich möglich ist
this
zu seinnull
. 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 mitthis
Sein einzugebennull
.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
null
nicht 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.String
Klasse in diesem Fall weiter zu verbessern .Ein weiterer Fall, in dem dies auftreten kann, sind umgekehrte Pinvoke-Aufrufe.
quelle
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:
call
undcallvirt
... Im Allgemeinen wird C # immer ausgegebencallvirt
, was erfordertthis
, dass es nicht null ist. Aber andere Sprachen (C ++ / CLIcall
fä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)
quelle
callvirt
hier emittieren . Immerhin ist es eine virtuelle Methode. Siehe meine Antwort.Der Quellcode hat diesen Kommentar:
quelle
Mal sehen ...
this
ist die erste Zeichenfolge, die Sie vergleichen.obj
ist das zweite Objekt. Es sieht also so aus, als wäre es eine Art Optimierung. Es wird zuerstobj
in einen Stringtyp umgewandelt. Und wenn das fehlschlägt,strB
ist null. Und wennstrB
nullthis
ist, während nicht, dann sind sie definitiv nicht gleich und dieEqualsHelper
Funktion kann übersprungen werden.Das spart einen Funktionsaufruf. Darüber hinaus
EqualsHelper
kö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. WennstrB
null 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ürstrB
die 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.quelle
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.
quelle
Equals
Implementierungen typisch ist . Das eigentliche Fleisch meiner Frage war, warum der Testthis != 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. Weitere Informationen finden Sie in der akzeptierten Antwort!