2016-02-01 5 views
121

z jakiegoś powodu został skradanie się do źródła Framework dla klasy Double i okazało się, że deklaracja == jest:Definicja „==” operatora dla podwójnego

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

Ta sama logika dotyczy każdy operator.


  • Jaki jest sens takiej definicji?
  • Jak to działa?
  • Dlaczego nie tworzy nieskończonej rekurencji?
+17

Spodziewałbym się niekończącej się rekurencji. – HimBromBeere

+0

Czy mam rację, to przesłoni porównanie z == tylko dla dwóch podwójnych wartości? Tak więc w powrocie można dodać specyfikację anotową –

+5

Jestem prawie pewien, że nie jest używany do porównywania w dowolnym miejscu z podwójnym, zamiast tego "ceq" jest wydawane w IL. To tylko po to, aby wypełnić jakiś cel dokumentacji, Nie można jednak znaleźć źródła. – Habib

Odpowiedz

60

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

Powód dla operatora w kodzie źródłowym jest prawdopodobny, więc można go wywołać z języków innych niż C#, które nie tłumaczą go bezpośrednio na wywołanie CEQ (lub poprzez odbicie). Kod w ramach operatora zostanie skompilowany do i skompilowany do CEQ, więc nie ma nieskończonej rekurencji.

W rzeczywistości, jeśli zadzwonić do operatora za pośrednictwem refleksji, można zobaczyć, że operator jest nazywany (zamiast instrukcji CEQ), no i oczywiście nie jest nieskończenie rekurencyjne (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})); 

Wynikające II (opracowany 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 

ciekawe - nie istnieją takie same operatorzy (zarówno do źródła odniesienia lub przez odbicia) integralnego typów tylko Single, Double, Decimal, String i DateTime, które obalają moją teorię, że istnieją, aby można je było nazwać innymi językami. Oczywiście można zrównać dwie liczby całkowite w innych językach bez tych operatorów, więc wracamy do pytania "dlaczego istnieją dla double"?

+11

Jedyny problem jaki widzę z tym, to że specyfikacja języka C# mówi, że przeciążone operatory mają pierwszeństwo przed wbudowanymi operatorami. Z pewnością zgodny kompilator C# powinien zauważyć, że przeciążony operator jest dostępny tutaj i generuje nieskończoną rekursję. Hmm. Kłopotliwe. –

+5

To nie odpowiada na pytanie, imho. Wyjaśnia tylko, do czego kod jest tłumaczony, ale nie dlaczego. Zgodnie z sekcją * 7.3.4 Binary Operator Overload Resolution * specyfikacji języka C# oczekiwałbym również nieskończonej rekursji. Zakładam, że źródło referencyjne (http://referencesource.microsoft.com/#mscorlib/system/double.cs,1a65cbdb09544ba1) tak naprawdę nie ma tutaj zastosowania. –

+0

@Damien_The_Unbeliever Jak stwierdziłem, podejrzewam, że IL generowane przez kod źródłowy _daje_ emituje operator 'CEQ', a zatem nie rekurencyjnie w nieskończoność. Tak więc albo jest to wyjątek do specyfikacji dla klasy "double" (wszystkie typy wartości, naprawdę), albo specyfikacja określa operatory bezpośrednio zdefiniowane na typach wartości jako operatory "wbudowane". –

8

Z JustDecompile przyjrzałem się CIL. Wewnętrzny kod == zostanie przetłumaczony na kod operacji CIL ceq. Innymi słowy, jest to prymitywna równość CLR.

Byłem ciekawy, czy kompilator C# odwołuje się do operatora ceq lub == podczas porównywania dwóch podwójnych 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 (uwaga oświadczenie 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 
12

Źródło typów pierwotnych może być mylące. Czy widziałeś już pierwszy wiersz struktury Double?

Normalnie nie można zdefiniować rekurencyjnej struct tak:

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

prymitywne typy mają swoje natywne wsparcie w CIL, jak również. Zwykle nie są one traktowane jak typy obiektowe. Podwojenie to tylko wartość 64-bitowa, jeśli jest używana jako CIL w postaci float64. Jeśli jednak jest obsługiwany jako zwykły typ .NET, zawiera on rzeczywistą wartość i zawiera metody, jak każdy inny typ.

To, co tu widzisz, to ta sama sytuacja dla operatorów. Zwykle, jeśli użyjesz bezpośrednio typu podwójnego, nigdy nie zostanie on wywołany. BTW, jego źródło wygląda to 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 jest nieskończona pętla (instrument ceq jest używany zamiast wywoływania System.Double::op_Equality). Więc gdy podwójne jest traktowane jak obiekt, zostanie wywołana metoda operatora, która ostatecznie obsłuży go jako typ pierwotny float64 na poziomie CIL.

+1

Dla tych, którzy nie rozumieją pierwszej części tego wpisu (może dlatego, że zazwyczaj nie piszą swoich 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 __error CS0523: Element struktury "MyNumber.m_value" typu "MyNumber" powoduje cykl w układzie struct__ –

37

Główne zamieszanie polega na tym, że zakładasz, że wszystkie biblioteki .NET (w tym przypadku Biblioteka Rozszerzonych Numerów, która jest nie część BCL) są napisane w standardowym C#. Nie zawsze tak jest, a różne języki mają różne reguły.

W standardowym C# fragment kodu, który widzisz, spowodowałby przepełnienie stosu, ze względu na sposób działania przeciążenia operatora. Jednak kod nie jest faktycznie w standardowym C# - w zasadzie wykorzystuje nieudokumentowane funkcje kompilatora C#. Zamiast wywoływać operatora, emituje ten kod:

ldarg.0 
ldarg.1 
ceq 
ret 

to jest to :) Nie ma 100% równoważny kod C# - to po prostu nie jest możliwe w C# z własnego typu.

Nawet wtedy rzeczywisty operator nie jest używany podczas kompilowania kodu C# - kompilator wykonuje kilka optymalizacji, jak w tym przypadku, gdzie zastępuje wywołanie op_Equality za pomocą prostego . Ponownie, nie można replikować tego w swojej własnej strukturze DoubleEx - jest to magia kompilatora.

To na pewno nie jest wyjątkowa sytuacja w .NET - jest mnóstwo kodu, który nie jest prawidłowy, standardowy C#. Powodem są zazwyczaj (a) hacki kompilatora i (b) inny język, z nieparzystymi (c) plikami uruchomieniowymi (patrzę na ciebie, Nullable!).

Od # kompilator Roslyn C jest oepn źródło, mogę rzeczywiście wskazać w miejscu, gdzie rozdzielczość przeciążenie decyduje:

The place where all binary operators are resolved

The "shortcuts" for intrinsic operators

Kiedy patrzysz na skróty, ty” Zobaczysz, że równość między podwójnym i podwójnym wynikiem w wewnętrznym podwójnym operatorze, nigdy nie jest w rzeczywistym operatora == zdefiniowanym na typie. System typu .NET musi udawać, że Double jest typem jak każdy inny, ale C# nie - double jest prymitywem w języku C#.

+1

Nie jestem pewien, czy zgadzam się, że kod w źródle odniesienia jest po prostu "poddany inżynierii wstecznej". Kod ma dyrektywy kompilacyjne ('# if's) i inne artefakty, które nie byłyby obecne w skompilowanym kodzie. Plus jeśli byłby odwrócony dla "podwójnego", to dlaczego nie został poddany inżynierii wstecznej dla 'int' lub' long'? Myślę, że istnieje powód, dla którego kod źródłowy, ale wierzę, że użycie '==' wewnątrz operatora zostanie skompilowane do 'CEQ', który zapobiega rekursji. Ponieważ operator jest "predefiniowanym" operatorem tego typu (i nie można go przesłonić), reguły przeciążania nie mają zastosowania. –

+0

@DStanley Nie chciałem sugerować, że * wszystkie * kod jest poddawany inżynierii wstecznej. I znowu "double" nie jest częścią BCL - znajduje się w osobnej bibliotece, która po prostu znajduje się w specyfikacji C#. Tak, '==' zostanie skompilowany do 'ceq', ale to nadal oznacza, że ​​jest to hackowanie kompilatora, którego nie możesz replikować w swoim własnym kodzie, i coś, co nie jest częścią specyfikacji C# (podobnie jak' pole float64' na strukturze 'Double'). To nie jest kontraktowa część C#, więc nie ma sensu traktować go jak poprawnego C#, nawet jeśli został skompilowany z kompilatorem C#. – Luaan

+0

@DStanely Nie mogłem znaleźć sposobu, w jaki struktura jest zorganizowana, ale w implementacji referencyjnej .NET 2.0 wszystkie trudne części są po prostu kompilatorami wewnętrznymi, zaimplementowanymi w C++. Oczywiście wciąż jest dużo kodu źródłowego .NET, ale rzeczy takie jak "porównywanie dwóch podwójnych" nie działałyby naprawdę w czystej .NET; to jeden z powodów, dla których numery zmiennoprzecinkowe nie są zawarte w BCL. Mimo to kod jest * również * zaimplementowany w (niestandardowym) języku C#, prawdopodobnie dokładnie z tego powodu, o którym wspomniałeś wcześniej - aby upewnić się, że inne kompilatory .NET mogą traktować te typy jako rzeczywiste typy .NET. – Luaan

0

Jak wskazano w dokumentacji Microsoft dla Przestrzeni nazw System.Runtime.Version: Typy znajdujące się w tej przestrzeni nazw są przeznaczone do użycia w.NET Framework, a nie dla aplikacji użytkownika. Przestrzeń nazw System.Runtime.Versioning zawiera zaawansowane typy, które obsługują wersjonowanie w równoległych implementacjach systemu .NET Framework.

+0

Co ma "System.Runtime.Versioning" zrobić z 'System.Double'? – Koopakiller