Definicja operatora „==” dla Double

126

Z jakiegoś powodu podkradałem się do źródła .NET Framework dla tej klasy Doublei odkryłem, że deklaracja ==to:

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

Ta sama logika obowiązuje dla każdego operatora.


  • Jaki jest sens takiej definicji?
  • Jak to działa?
  • Dlaczego nie tworzy nieskończonej rekurencji?
Thomas Ayoub
źródło
17
Spodziewałbym się niekończącej się rekurencji.
HimBromBeere
5
Jestem prawie pewien, że nie jest nigdzie używany do porównania z podwójnym, zamiast tego ceqjest wydawany w IL. Jest to tylko po to, aby wypełnić jakieś cele dokumentacyjne, nie mogę jednak znaleźć źródła.
Habib
2
Najprawdopodobniej tak, że ten operator można uzyskać za pomocą odbicia.
Damien_The_Unbeliever
3
To nigdy nie zostanie wywołane, kompilator ma logikę równości zapieczętowaną w (kod operacji ceq) zobacz Kiedy wywoływany jest operator == Double?
Alex K.
1
@ZoharPeled dzielenie podwójnej przez zero jest poprawne i spowoduje dodatnią lub ujemną nieskończoność.
Magnus

Odpowiedzi:

62

W rzeczywistości kompilator zamieni ==operator w ceqkod IL, a operator, o którym wspomniałeś, nie zostanie wywołany.

Przyczyną istnienia operatora w kodzie źródłowym jest prawdopodobnie to, że można go wywołać z języków innych niż C #, które nie tłumaczą go CEQbezpośrednio na wywołanie (lub przez odbicie). Kod wewnątrz operatora zostanie skompilowany do a CEQ, więc nie ma nieskończonej rekursji.

W rzeczywistości, jeśli wywołasz operator przez odbicie, zobaczysz, że operator jest wywoływany (a nie CEQinstrukcja) i oczywiście nie jest nieskończenie rekurencyjny (ponieważ program kończy się zgodnie z oczekiwaniami):

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

Wynikowy IL (skompilowany przez 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 

Co ciekawe - nie istnieją same podmioty (zarówno w źródle referencyjnym lub poprzez odbicie) dla integralnych typów, tylko Single, Double, Decimal, String, i DateTime, co obala moją teorię, że istnieją one być wywoływane z innych języków. Oczywiście bez tych operatorów można zrównać dwie liczby całkowite w innych językach, więc wracamy do pytania „dlaczego one istnieją dla double”?

D Stanley
źródło
12
Jedynym problemem, jaki widzę w tym przypadku, jest to, że specyfikacja języka C # mówi, że przeciążone operatory mają pierwszeństwo przed operatorami wbudowanymi. Więc z pewnością zgodny kompilator C # powinien zobaczyć, że przeciążony operator jest tutaj dostępny i wygenerować nieskończoną rekursję. Hmm. Niepokojące.
Damien_The_Unbeliever
5
To nie odpowiada na pytanie, imho. Wyjaśnia tylko, na co jest tłumaczony kod, ale nie dlaczego. Zgodnie z sekcją 7.3.4 Rozpoznawanie przeciążeń operatorów binarnych specyfikacji języka C # spodziewałbym się również nieskończonej rekurencji. Zakładam, że źródło odniesienia ( referenceource.microsoft.com/#mscorlib/system/… ) tak naprawdę nie ma tutaj zastosowania.
Dirk Vollmar
6
@DStanley - Nie zaprzeczam temu, co jest produkowane. Mówię, że nie mogę tego pogodzić ze specyfikacją języka. To właśnie jest niepokojące. Myślałem o przejrzeniu Roslyn i sprawdzeniu, czy mogę znaleźć tutaj jakąś specjalną obsługę, ale nie jestem teraz dobrze przygotowany do tego (zła maszyna)
Damien_The_Unbeliever
1
@Damien_The_Unbeliever Dlatego uważam, że jest to albo wyjątek od specyfikacji, albo inna interpretacja operatorów „wbudowanych”.
D Stanley
1
Ponieważ @Jon Skeet jeszcze nie odpowiedział ani nie skomentował tego, podejrzewam, że jest to błąd (tj. Naruszenie specyfikacji).
TheBlastOne
37

Głównym problemem jest to, że zakładasz, że wszystkie biblioteki .NET (w tym przypadku Extended Numerics Library, która nie jest częścią BCL) są napisane w standardowym języku C #. Nie zawsze tak jest, a różne języki mają różne zasady.

W standardowym języku C # fragment kodu, który widzisz, spowodowałby przepełnienie stosu ze względu na sposób działania rozpoznawania przeciążenia operatora. Jednak kod nie jest w rzeczywistości w standardowym języku C # - w zasadzie wykorzystuje nieudokumentowane funkcje kompilatora C #. Zamiast dzwonić do operatora, emituje następujący kod:

ldarg.0
ldarg.1
ceq
ret

To wszystko :) Nie ma 100% odpowiednika kodu C # - to po prostu nie jest możliwe w C # z własnym typem.

Nawet wtedy rzeczywisty operator nie jest używany podczas kompilowania kodu C # - kompilator wykonuje kilka optymalizacji, jak w tym przypadku, gdzie zastępuje op_Equalitywywołanie prostym ceq. Ponownie, nie możesz tego powtórzyć we własnej DoubleExstrukturze - to magia kompilatora.

Z pewnością nie jest to wyjątkowa sytuacja w .NET - jest mnóstwo nieprawidłowego kodu, standardowy C #. Powody są zwykle (a) hacki kompilatora i (b) inny język, z dziwnymi (c) hackami środowiska uruchomieniowego (patrzę na ciebie Nullable!).

Ponieważ kompilator Roslyn C # jest źródłem oepn, mogę wskazać miejsce, w którym zdecydowano o rozwiązaniu problemu:

Miejsce, w którym wszystkie operatory binarne są rozwiązywane

„Skróty” dla operatorów wewnętrznych

Kiedy spojrzysz na skróty, zobaczysz, że równość między double i double skutkuje wewnętrznym operatorem double, nigdy faktycznym ==operatorem zdefiniowanym w typie. System typów .NET musi udawać, że Doublejest typem jak każdy inny, ale C # tego nie robi - doublejest prymitywem w C #.

Luaan
źródło
1
Nie jestem pewien, czy zgadzam się, że kod w źródle odniesienia jest po prostu „odtworzony”. Kod zawiera dyrektywy kompilatora #ifi inne artefakty, które nie byłyby obecne w skompilowanym kodzie. Poza tym, jeśli został poddany inżynierii wstecznej, doubledlaczego nie został zaprojektowany dla intlub long? Myślę, że jest powód dla kodu źródłowego, ale uważam, że użycie ==operatora wewnątrz zostaje skompilowane do a, CEQco zapobiega rekursji. Ponieważ operator jest operatorem „predefiniowanym” dla tego typu (i nie można go zastąpić), reguły przeciążenia nie mają zastosowania.
D Stanley
@DStanley Nie chciałem sugerować, że cały kod jest poddawany inżynierii wstecznej. I znowu, doublenie jest częścią BCL - znajduje się w oddzielnej bibliotece, która akurat znajduje się w specyfikacji C #. Tak, ==zostaje skompilowany do a ceq, ale to nadal oznacza, że ​​jest to hack kompilatora, którego nie można replikować we własnym kodzie, i coś, co nie jest częścią specyfikacji C # (podobnie jak float64pole w Doublestrukturze). Nie jest to umowna część C #, więc nie ma sensu traktować go jak prawidłowego C #, nawet jeśli został skompilowany za pomocą kompilatora C #.
Luaan
@DStanely Nie mogłem znaleźć sposobu organizacji rzeczywistego frameworka, ale w implementacji referencyjnej .NET 2.0, wszystkie skomplikowane części to tylko wewnętrzne elementy kompilatora, zaimplementowane w C ++. Oczywiście nadal istnieje wiele natywnego kodu .NET, ale rzeczy takie jak „porównywanie dwóch podwójnych” nie działałyby dobrze w czystym .NET; to jeden z powodów, dla których liczby zmiennoprzecinkowe nie są uwzględnione w BCL. To powiedziawszy, kod jest również zaimplementowany w (niestandardowym) C #, prawdopodobnie dokładnie z powodu, o którym wspomniałeś wcześniej - aby upewnić się, że inne kompilatory .NET mogą traktować te typy jako rzeczywiste typy .NET.
Luaan
@DStanley Ale okej, uwaga. Usunąłem odwołanie „odtworzone” i przeredagowałem odpowiedź, aby wyraźnie wspomnieć o „standardowym języku C #”, a nie tylko w języku C #. I nie traktuj doubletego w ten sam sposób, co inti long- inti longsą to typy prymitywne, które muszą obsługiwać wszystkie języki .NET. float, decimalA doublenie są.
Luaan
12

Źródło typów pierwotnych może być mylące. Widziałeś pierwszą linię Doublestruktury?

Zwykle nie można zdefiniować struktury rekurencyjnej w ten sposób:

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

Typy prymitywne mają również natywne wsparcie w CIL. Zwykle nie są traktowane jak typy zorientowane obiektowo. Double to tylko 64-bitowa wartość, jeśli jest używana tak jak float64w CIL. Jeśli jednak jest obsługiwany jak zwykły typ .NET, zawiera rzeczywistą wartość i zawiera metody, jak wszystkie inne typy.

Więc to, co widzisz tutaj, to ta sama sytuacja dla operatorów. Zwykle jeśli używasz typu podwójnego bezpośrednio, nigdy nie zostanie wywołany. Przy okazji, jego źródło wygląda tak w CIL:

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

Jak widać, nie ma nieskończonej pętli ( ceqinstrument jest używany zamiast wywoływania System.Double::op_Equality). Więc kiedy double jest traktowany jak obiekt, zostanie wywołana metoda operatora, która ostatecznie obsłuży go jako float64typ pierwotny na poziomie CIL.

György Kőszeg
źródło
1
Dla tych, którzy nie rozumieją pierwszej części tego postu (być może dlatego, że zwykle nie piszą własnych typów wartości), wypróbuj kod public struct MyNumber { internal MyNumber m_value; }. Oczywiście nie można go skompilować. Błąd to błąd CS0523: element struktury „MyNumber.m_value” typu „MyNumber” powoduje cykl w układzie struktury
Jeppe Stig Nielsen
8

Wziąłem spojrzeć na CIL z JustDecompile. Wewnętrzna ==jest tłumaczona na kod operacyjny CIL ceq . Innymi słowy, jest to pierwotna równość CLR.

Byłem ciekawy, czy kompilator C # będzie odwoływał się ceqdo ==operatora, czy też porównując dwie podwójne wartości. W trywialnym przykładzie, który wymyśliłem (poniżej), użyłem ceq.

Ten program:

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

generuje następujący CIL (zwróć uwagę na instrukcję z etykietą 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
źródło
-2

Jak wskazano w dokumentacji firmy Microsoft dotyczącej przestrzeni nazw System.Runtime.Versioning: Typy znajdujące się w tej przestrzeni nazw są przeznaczone do użytku w ramach platformy .NET Framework, a nie aplikacji użytkownika. Przestrzeń nazw System.Runtime.Versioning zawiera zaawansowane typy obsługujące przechowywanie wersji w równoległe implementacje .NET Framework.

Thomas Papamihos
źródło
Co to ma System.Runtime.Versioningwspólnego System.Double?
Koopakiller