Definition des Operators "==" für Double

126

Aus irgendeinem Grund habe ich mich in die .NET Framework-Quelle für die Klasse geschlichen Doubleund festgestellt, dass die Deklaration von ==:

public static bool operator ==(Double left, Double right) {
    return left == right;
}

Die gleiche Logik gilt für jeden Bediener.


  • Was ist der Sinn einer solchen Definition?
  • Wie funktioniert es?
  • Warum erzeugt es keine unendliche Rekursion?
Thomas Ayoub
quelle
17
Ich würde eine endlose Rekursion erwarten.
HimBromBeere
5
Ich bin mir ziemlich sicher, dass es nirgendwo zum Vergleich mit double verwendet wird, sondern ceqin IL ausgegeben wird. Dies ist nur dazu da, um einen Dokumentationszweck zu erfüllen. Die Quelle kann jedoch nicht gefunden werden.
Habib
2
Höchstwahrscheinlich, damit dieser Operator durch Reflexion erhalten werden kann.
Damien_The_Unbeliever
3
Das wird niemals aufgerufen, der Compiler hat die Gleichheitslogik eingebrannt (ceq opcode), siehe Wann wird der == Operator von Double aufgerufen?
Alex K.
1
@ZoharPeled Das Teilen eines Doppels mit Null ist gültig und führt zu einer positiven oder negativen Unendlichkeit.
Magnus

Antworten:

62

In Wirklichkeit verwandelt der Compiler den ==Operator in einen ceqIL-Code, und der von Ihnen erwähnte Operator wird nicht aufgerufen.

Der Grund für den Operator im Quellcode ist wahrscheinlich, dass er aus anderen Sprachen als C # aufgerufen werden kann, die ihn nicht CEQdirekt (oder durch Reflexion) in einen Aufruf übersetzen . Der Code innerhalb des Operators wird zu a kompiliert CEQ, sodass es keine unendliche Rekursion gibt.

Wenn Sie den Operator über Reflection aufrufen, können Sie tatsächlich sehen, dass der Operator aufgerufen wird (und nicht eine CEQAnweisung) und offensichtlich nicht unendlich rekursiv ist (da das Programm wie erwartet beendet wird):

double d1 = 1.1;
double d2 = 2.2;

MethodInfo mi = typeof(Double).GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public );

bool b = (bool)(mi.Invoke(null, new object[] {d1,d2}));

Resultierende IL (zusammengestellt von LinqPad 4):

IL_0000:  nop         
IL_0001:  ldc.r8      9A 99 99 99 99 99 F1 3F 
IL_000A:  stloc.0     // d1
IL_000B:  ldc.r8      9A 99 99 99 99 99 01 40 
IL_0014:  stloc.1     // d2
IL_0015:  ldtoken     System.Double
IL_001A:  call        System.Type.GetTypeFromHandle
IL_001F:  ldstr       "op_Equality"
IL_0024:  ldc.i4.s    18 
IL_0026:  call        System.Type.GetMethod
IL_002B:  stloc.2     // mi
IL_002C:  ldloc.2     // mi
IL_002D:  ldnull      
IL_002E:  ldc.i4.2    
IL_002F:  newarr      System.Object
IL_0034:  stloc.s     04 // CS$0$0000
IL_0036:  ldloc.s     04 // CS$0$0000
IL_0038:  ldc.i4.0    
IL_0039:  ldloc.0     // d1
IL_003A:  box         System.Double
IL_003F:  stelem.ref  
IL_0040:  ldloc.s     04 // CS$0$0000
IL_0042:  ldc.i4.1    
IL_0043:  ldloc.1     // d2
IL_0044:  box         System.Double
IL_0049:  stelem.ref  
IL_004A:  ldloc.s     04 // CS$0$0000
IL_004C:  callvirt    System.Reflection.MethodBase.Invoke
IL_0051:  unbox.any   System.Boolean
IL_0056:  stloc.3     // b
IL_0057:  ret 

Interessanterweise - die gleichen Betreiber nicht existieren (entweder in der Referenzquelle oder über Reflexion) für integrale Typen, nur Single, Double, Decimal, String, und DateTime, die widerlegt meine Theorie , dass sie aus anderen Sprachen aufgerufen werden , existieren. Natürlich können Sie ohne diese Operatoren zwei Ganzzahlen in anderen Sprachen gleichsetzen. Wir kehren also zu der Frage zurück, warum sie existieren double.

D Stanley
quelle
12
Das einzige Problem, das ich dabei sehen kann, ist, dass die C # -Sprachenspezifikation besagt, dass überladene Operatoren Vorrang vor integrierten Operatoren haben. Ein konformer C # -Compiler sollte also sicher sehen, dass hier ein überladener Operator verfügbar ist, und die unendliche Rekursion generieren. Hmm. Beunruhigend.
Damien_The_Unbeliever
5
Das beantwortet die Frage nicht, imho. Es wird nur erklärt, in was der Code übersetzt wird, aber nicht warum. Gemäß Abschnitt 7.3.4 Überlastungsauflösung des Binäroperators der C # -Sprachspezifikation würde ich auch eine unendliche Rekursion erwarten. Ich würde annehmen, dass die Referenzquelle ( referencesource.microsoft.com/#mscorlib/system/… ) hier nicht wirklich gilt.
Dirk Vollmar
6
@DStanley - Ich leugne nicht, was produziert wird. Ich sage, ich kann es nicht mit der Sprachspezifikation vereinbaren. Das ist es, was beunruhigt. Ich habe darüber nachgedacht, durch Roslyn zu stöbern und zu sehen, ob ich hier eine spezielle Behandlung finden könnte, aber ich bin derzeit nicht gut darauf eingestellt (falsche Maschine)
Damien_The_Unbeliever
1
@Damien_The_Unbeliever Deshalb denke ich, dass es entweder eine Ausnahme von der Spezifikation oder eine andere Interpretation von "eingebauten" Operatoren ist.
D Stanley
1
Da @Jon Skeet dies noch nicht beantwortet oder kommentiert hat, vermute ich, dass es sich um einen Fehler handelt (dh eine Verletzung der Spezifikation).
TheBlastOne
37

Die Hauptverwirrung besteht darin, dass Sie davon ausgehen, dass alle .NET-Bibliotheken (in diesem Fall die Extended Numerics Library, die nicht Teil der BCL ist) in Standard-C # geschrieben sind. Dies ist nicht immer der Fall und verschiedene Sprachen haben unterschiedliche Regeln.

In Standard-C # würde der angezeigte Code aufgrund der Funktionsweise der Operatorüberlastungsauflösung zu einem Stapelüberlauf führen. Der Code befindet sich jedoch nicht in Standard-C # - er verwendet grundsätzlich undokumentierte Funktionen des C # -Compilers. Anstatt den Operator aufzurufen, wird der folgende Code ausgegeben:

ldarg.0
ldarg.1
ceq
ret

Das war's :) Es gibt keinen 100% äquivalenten C # -Code - dies ist in C # mit Ihrem eigenen Typ einfach nicht möglich .

Selbst dann wird der eigentliche Operator beim Kompilieren von C # -Code nicht verwendet - der Compiler führt eine Reihe von Optimierungen durch, wie in diesem Fall, bei denen der op_EqualityAufruf nur durch den einfachen ersetzt wird ceq. Auch hier können Sie dies nicht in Ihrer eigenen DoubleExStruktur replizieren - es ist Compiler-Magie.

Dies ist sicherlich keine einzigartige Situation in .NET - es gibt viele ungültige Codes, Standard-C #. Die Gründe sind normalerweise (a) Compiler-Hacks und (b) eine andere Sprache mit den ungeraden (c) Laufzeit-Hacks (ich sehe dich an Nullable!).

Da der Roslyn C # -Compiler eine Oepn-Quelle ist, kann ich Sie tatsächlich auf den Ort verweisen, an dem die Überlastungsauflösung entschieden wird:

Der Ort, an dem alle Binäroperatoren aufgelöst werden

Die "Verknüpfungen" für intrinsische Operatoren

Wenn Sie sich die Verknüpfungen ansehen, werden Sie feststellen, dass die Gleichheit zwischen double und double zu dem intrinsischen double-Operator führt, niemals zu dem tatsächlichen ==Operator, der für den Typ definiert ist. Das .NET-Typsystem muss so tun, als wäre Doublees ein Typ wie jeder andere, aber C # nicht - doubleist ein Grundelement in C #.

Luaan
quelle
1
Ich bin mir nicht sicher, ob ich damit einverstanden bin, dass der Code in der Referenzquelle nur "rückentwickelt" ist. Der Code enthält Compiler-Direktiven #ifund andere Artefakte, die im kompilierten Code nicht vorhanden wären. Und wenn es rückentwickelt wurde, doublewarum wurde es dann nicht rückentwickelt intoder long? Ich denke, es gibt einen Grund für den Quellcode, aber ich glaube, dass die Verwendung von ==innerhalb des Operators zu einem kompiliert wird, CEQder eine Rekursion verhindert. Da der Operator ein "vordefinierter" Operator für diesen Typ ist (und nicht überschrieben werden kann), gelten die Überladungsregeln nicht.
D Stanley
@DStanley Ich wollte nicht implizieren, dass der gesamte Code rückentwickelt ist. Und wieder doubleist es nicht Teil der BCL - es befindet sich in einer separaten Bibliothek, die zufällig in der C # -Spezifikation enthalten ist. Ja, das ==wird zu a kompiliert ceq, aber das bedeutet immer noch, dass dies ein Compiler-Hack ist, den Sie nicht in Ihrem eigenen Code replizieren können, und etwas, das nicht Teil der C # -Spezifikation ist (genau wie das float64Feld in der DoubleStruktur). Es ist kein vertraglicher Bestandteil von C #, daher macht es wenig Sinn, es als gültiges C # zu behandeln, selbst wenn es mit dem C # -Compiler kompiliert wurde.
Luaan
@DStanely Ich konnte nicht finden, wie das reale Framework organisiert ist, aber in der Referenzimplementierung von .NET 2.0 sind alle kniffligen Teile nur Compiler-Eigenheiten, die in C ++ implementiert sind. Natürlich gibt es immer noch viel nativen .NET-Code, aber Dinge wie "Vergleichen von zwei Doppelwerten" würden in reinem .NET nicht wirklich gut funktionieren. Dies ist einer der Gründe, warum Gleitkommazahlen nicht in der BCL enthalten sind. Der Code ist jedoch auch in (nicht standardisiertem) C # implementiert, wahrscheinlich genau aus dem zuvor genannten Grund, um sicherzustellen, dass andere .NET-Compiler diese Typen als echte .NET-Typen behandeln können.
Luaan
@ DStanley Aber okay, Punkt genommen. Ich entfernte die "Reverse Engineered" -Referenz und formulierte die Antwort neu, um explizit "Standard-C #" und nicht nur C # zu erwähnen. Und behandeln Sie nicht doublewie intund long- intund longsind primitive Typen, die alle .NET-Sprachen unterstützen müssen. float, decimalUnd doublesind es nicht.
Luaan
12

Die Quelle der primitiven Typen kann verwirrend sein. Hast du die allererste Zeile der DoubleStruktur gesehen?

Normalerweise können Sie eine rekursive Struktur wie folgt nicht definieren:

public struct Double : IComparable, IFormattable, IConvertible
        , IComparable<Double>, IEquatable<Double>
{
    internal double m_value; // Self-recursion with endless loop?
    // ...
}

Primitive Typen haben ihre native Unterstützung auch in CIL. Normalerweise werden sie nicht wie objektorientierte Typen behandelt. Ein Double ist nur ein 64-Bit-Wert, wenn es wie float64in CIL verwendet wird. Wenn es jedoch als normaler .NET-Typ behandelt wird, enthält es einen tatsächlichen Wert und Methoden wie alle anderen Typen.

Was Sie hier sehen, ist die gleiche Situation für die Bediener. Wenn Sie den Typ mit doppeltem Typ direkt verwenden, wird er normalerweise nie aufgerufen. Übrigens sieht die Quelle in CIL folgendermaßen aus:

.method public hidebysig specialname static bool op_Equality(float64 left, float64 right) cil managed
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor()
    .custom instance void __DynamicallyInvokableAttribute::.ctor()
    .maxstack 8
    L_0000: ldarg.0
    L_0001: ldarg.1
    L_0002: ceq
    L_0004: ret
}

Wie Sie sehen können, gibt es keine Endlosschleife (das ceqInstrument wird verwendet, anstatt das aufzurufen System.Double::op_Equality). Wenn also ein Double wie ein Objekt behandelt wird, wird die Operatormethode aufgerufen, die es schließlich als float64primitiven Typ auf CIL-Ebene behandelt.

György Kőszeg
quelle
1
Versuchen Sie den Code für diejenigen, die den ersten Teil dieses Beitrags nicht verstehen (möglicherweise, weil sie normalerweise keine eigenen Werttypen schreiben) public struct MyNumber { internal MyNumber m_value; }. Es kann natürlich nicht kompiliert werden. Der Fehler ist Fehler CS0523: Strukturelement 'MyNumber.m_value' vom Typ 'MyNumber' verursacht einen Zyklus im Strukturlayout
Jeppe Stig Nielsen
8

Ich habe mir die CIL mit JustDecompile angesehen. Das Innere ==wird in den CIL- Ceq- Op-Code übersetzt. Mit anderen Worten, es ist primitive CLR-Gleichheit.

Ich war gespannt, ob der C # -Compiler darauf verweisen würde ceq== beim Vergleich zweier Doppelwerte auf den Operator . In dem trivialen Beispiel, das ich mir (unten) ausgedacht habe, wurde es verwendet ceq.

Dieses Programm:

void Main()
{
    double x = 1;
    double y = 2;

    if (x == y)
        Console.WriteLine("Something bad happened!");
    else
        Console.WriteLine("All is right with the world");
}

generiert die folgende CIL (beachten Sie die Anweisung mit dem Label IL_0017):

IL_0000:  nop
IL_0001:  ldc.r8      00 00 00 00 00 00 F0 3F
IL_000A:  stloc.0     // x
IL_000B:  ldc.r8      00 00 00 00 00 00 00 40
IL_0014:  stloc.1     // y
IL_0015:  ldloc.0     // x
IL_0016:  ldloc.1     // y
IL_0017:  ceq
IL_0019:  stloc.2
IL_001A:  ldloc.2
IL_001B:  brfalse.s   IL_002A
IL_001D:  ldstr       "Something bad happened!"
IL_0022:  call        System.Console.WriteLine
IL_0027:  nop
IL_0028:  br.s        IL_0035
IL_002A:  ldstr       "All is right with the world"
IL_002F:  call        System.Console.WriteLine
IL_0034:  nop
IL_0035:  ret
Daniel Pratt
quelle
-2

Wie in der Microsoft-Dokumentation für den System.Runtime.Versioning-Namespace angegeben: Die in diesem Namespace gefundenen Typen sind für die Verwendung in .NET Framework und nicht für Benutzeranwendungen vorgesehen. Der System.Runtime.Versioning-Namespace enthält erweiterte Typen, die die Versionierung unterstützen Side-by-Side-Implementierungen von .NET Framework.

Thomas Papamihos
quelle
Was hat damit System.Runtime.Versioningzu tun System.Double?
Koopakiller