2016-02-01 14 views
121

Per qualche ragione mi è stato furtivamente nella fonte .NET Framework per la classe Double e ha scoperto che la dichiarazione di == è:definizione di operatore "==" per Double

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

La stessa logica vale per ogni operatore.


  • Qual è il punto di tale definizione?
  • Come funziona?
  • Perché non crea una ricorsione infinita?
+17

Mi aspetterei una ricorsione senza fine. – HimBromBeere

+0

ho ragione, questo prevale sul confronto con == solo per due valori doppi? Quindi nel ritorno è possibile aggiungere le specifiche anoter –

+5

Sono abbastanza sicuro che non è usato per il confronto ovunque con il doppio, invece 'ceq' è emesso in IL. Questo è solo lì per riempire alcuni scopi di documentazione, non è possibile trovare la fonte però. – Habib

risposta

60

In realtà, il compilatore si accende l'operatore == in un codice IL ceq, e l'operatore che menzioni non sarà chiamato.

Il motivo per l'operatore nel codice sorgente è probabile in modo che possa essere chiamato da lingue diverse da C# che non lo traducono in una chiamata CEQ direttamente (o tramite riflessione). Il codice all'interno di l'operatore sarà compilato in un CEQ, quindi non vi è ricorsione infinita.

In realtà, se si chiama l'operatore attraverso la riflessione, si può vedere che l'operatore è chiamato (invece di un'istruzione CEQ), e ovviamente non è infinitamente ricorsiva (dal momento che il programma termina come previsto):

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

risultante iL (compilato da 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 

interessante - non esistono gli stessi operatori (sia nel sorgente di riferimento o tramite riflessione) per tipi integrali, solo Single, Double, Decimal, String e DateTime, che smentisce la mia teoria che esistano per essere chiamati da altre lingue. Ovviamente è possibile equiparare due interi in altre lingue senza questi operatori, quindi torniamo alla domanda "perché esistono per double"?

+11

L'unico problema che posso vedere con questo è che le specifiche del linguaggio C# dicono che gli operatori sovraccaricati hanno la precedenza sugli operatori integrati. Quindi sicuramente un compilatore C# conforme dovrebbe vedere che un operatore sovraccarico è disponibile qui e genera la ricorsione infinita. Hmm. Preoccupante. –

+5

Questo non risponde alla domanda, imho. Spiega solo in cosa viene tradotto il codice ma non perché. Secondo la sezione * 7.3.4 Risoluzione di sovraccarico dell'operatore binario * delle specifiche del linguaggio C# anch'io mi aspetterei una ricorsione infinita. Presumo che la fonte di riferimento (http://referencesource.microsoft.com/#mscorlib/system/double.cs.1a65cbdb09544ba1) non si applichi qui. –

+0

@Damien_The_Unbeliever Come già detto, sospetto che l'IL generato dal codice sorgente _does_ emetta un operatore 'CEQ' e quindi non riceva all'infinito. Quindi o è un'eccezione alla specifica per la classe 'double' (tutti i tipi di valore, in realtà), oppure la specifica considera gli operatori direttamente definiti sui tipi di valore come operatori" built-in ". –

8

Ho dato un'occhiata allo CIL con JustDecompile. L'interno == viene tradotto nel codice op CIL ceq. In altre parole, è l'uguaglianza primitiva dei CLR.

Ero curioso di vedere se il compilatore C# faceva riferimento allo ceq o all'operatore == quando confronta due valori doppi. Nel banale esempio che ho trovato (sotto), è stato utilizzato ceq.

Questo programma:

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

genera il seguente CIL (si noti la dichiarazione con etichetta 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

La fonte dei tipi primitivi può essere fonte di confusione. Hai visto la prima riga della struttura Double?

Normalmente non è possibile definire una struttura ricorsiva come questo:

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

tipi primitivi hanno il loro supporto nativo nel CIL pure. Normalmente non sono trattati come tipi orientati agli oggetti. Un doppio è solo un valore a 64 bit se viene utilizzato come float64 in CIL. Tuttavia, se viene gestito come un normale tipo .NET, contiene un valore effettivo e contiene metodi come qualsiasi altro tipo.

Quindi quello che vedi qui è la stessa situazione per gli operatori. Normalmente se si usa direttamente il tipo di doppio tipo, non verrà mai chiamato. A proposito, la sua fonte si presenta così in 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 
} 

Come si può vedere, non v'è alcun ciclo infinito (lo strumento ceq viene utilizzato invece di chiamare il System.Double::op_Equality). Quindi, quando un doppio viene trattato come un oggetto, verrà chiamato il metodo operatore, che alla fine lo gestirà come il tipo primitivo float64 a livello CIL.

+1

Per coloro che non comprendono la prima parte di questo post (forse perché di solito non scrivono i propri tipi di valore), prova il codice 'public struct MyNumber {internal MyNumber m_value; } '. Non può essere compilato, ovviamente. L'errore è __error CS0523: Il membro Struct 'MyNumber.m_value' di tipo 'MyNumber' provoca un ciclo nel struct layout__ –

37

La confusione principale qui è che si presume che tutte le librerie .NET (in questo caso, la libreria di numeri estesi, che è non una parte del BCL) siano scritte in C# standard. Questo non è sempre il caso, e lingue diverse hanno regole diverse.

In C# standard, la parte di codice che si sta visualizzando comporterebbe un sovraccarico dello stack, a causa del modo in cui la risoluzione di sovraccarico dell'operatore funziona. Tuttavia, il codice non è in realtà in C# standard - fondamentalmente utilizza funzionalità non documentate del compilatore C#. Invece di chiamare l'operatore, emette questo codice:

ldarg.0 
ldarg.1 
ceq 
ret 

Questo è tutto :) Non esiste un equivalente codice C# 100% - questo semplicemente non è possibile in C# con il proprio tipo.

Anche in questo caso, l'operatore effettivo non viene utilizzato durante la compilazione del codice C# - il compilatore esegue una serie di ottimizzazioni, come in questo caso, in cui sostituisce la chiamata op_Equality con il semplice ceq. Ancora una volta, non è possibile replicare questo nella propria struttura DoubleEx - è magia del compilatore.

Questo certamente non è una situazione unica in .NET - c'è un sacco di codice che non è valido, C# standard. Le ragioni sono solitamente (a) hack del compilatore e (b) una lingua diversa, con gli hack runtime (c) dispari (ti sto guardando, Nullable!).

Dal momento che l'Roslyn compilatore C# è fonte oepn, posso effettivamente puntare nel luogo in cui la risoluzione di sovraccarico è deciso:

The place where all binary operators are resolved

The "shortcuts" for intrinsic operators

Quando si guardano le scorciatoie, si' Vedremo che l'uguaglianza tra doppio e doppio risulta nell'operatore intrinseco doppio, mai nell'effettivo == definito dal tipo. Il sistema di tipo .NET deve fingere che Double sia un tipo come un altro, ma C# no - double è una primitiva in C#.

+1

Non sono sicuro di essere d'accordo che il codice nell'origine di riferimento è solo "reverse engineering". Il codice ha direttive del compilatore ('# if's) e altri artefatti che non sarebbero presenti nel codice compilato. Inoltre se è stato decodificato per 'double', allora perché non è stato decodificato per' int' o 'long'? Penso che ci sia un motivo per il codice sorgente ma crediamo che l'uso di '==' all'interno dell'operatore venga compilato in un 'CEQ' che impedisce la ricorsione. Poiché l'operatore è un operatore "predefinito" per quel tipo (e non può essere ignorato) le regole di sovraccarico non si applicano. –

+0

@DStanley Non volevo implicare che * tutto * il codice sia stato decodificato. E ancora, 'double' non fa parte del BCL - si trova in una libreria separata, che sembra essere inclusa nella specifica C#. Sì, il '==' viene compilato in un 'ceq', ma ciò significa comunque che questo è un hack del compilatore che non è possibile replicare nel proprio codice e qualcosa che non fa parte della specifica C# (proprio come il' float64' sulla struttura 'Double'). Non è una parte contrattuale di C#, quindi non ha senso trattarlo come C# valido, anche se è stato compilato con il compilatore C#. – Luaan

+0

@DStanely Non sono riuscito a trovare il modo in cui è organizzato il framework reale, ma nell'implementazione di riferimento di .NET 2.0, tutte le parti difficili sono solo intrinseche del compilatore, implementate in C++. C'è ancora un sacco di codice nativo .NET, ovviamente, ma cose come "confrontare due doppi" non funzionerebbero molto bene in puro .NET; questo è uno dei motivi per cui i numeri in virgola mobile non sono inclusi nel BCL. Detto questo, il codice è * anche * implementato in C# (non standard), probabilmente esattamente per il motivo che hai menzionato prima - per assicurarsi che altri compilatori .NET possano trattare questi tipi come veri tipi .NET. – Luaan

0

Come indicato nella documentazione Microsoft per System.Runtime.Versioning Namespace: I tipi trovati in questo spazio dei nomi sono destinati all'uso all'interno di.NET Framework e non per le applicazioni utente. Lo spazio dei nomi System.Runtime.Versioning contiene tipi avanzati che supportano il controllo delle versioni nelle implementazioni affiancate di .NET Framework.

+0

Cosa ha a che fare con 'System.Runtime.Versioning' con' System.Double'? – Koopakiller

Problemi correlati