Definizione dell'operatore "==" per Double


126

Per qualche motivo stavo entrando di nascosto nel sorgente .NET Framework per la classe Doublee ho scoperto che la dichiarazione di ==è:

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 aspetto una ricorsione senza fine.
HimBromBeere

5
Sono abbastanza sicuro che non sia usato per il confronto da nessuna parte con il doppio, invece ceqè emesso in IL. Questo è solo lì per riempire un po 'di scopo della documentazione, ma non riesco a trovare la fonte.
Habib,

2
Molto probabilmente in modo che questo operatore possa essere ottenuto tramite Reflection.
Damien_The_Unbeliever,

3
Questo non verrà mai chiamato, il compilatore ha la logica dell'uguaglianza cotta in (ceq opcode) vedi Quando viene invocato l'operatore == di Double?
Alex K.

1
@ZoharPeled La divisione di un doppio con zero è valida e comporterà l'infinito positivo o negativo.
Magnus,

Risposte:


62

In realtà, il compilatore trasformerà l' ==operatore in un ceqcodice IL e l'operatore menzionato non verrà chiamato.

Il motivo per cui l'operatore nel codice sorgente è probabilmente così può essere chiamato da lingue diverse da C # che non lo traducono CEQdirettamente in una chiamata (o tramite la riflessione). Il codice all'interno dell'operatore verrà compilato in un CEQ, quindi non c'è ricorsione infinita.

Infatti, se si chiama l'operatore tramite reflection, è possibile vedere che l'operatore viene chiamato (anziché CEQun'istruzione) e ovviamente non è infinitamente ricorsivo (poiché 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}));

IL risultante (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 notare - non esistono gli stessi operatori (sia nel sorgente di riferimento o attraverso la riflessione) per i tipi integrali, solo Single, Double, Decimal, String, e DateTime, che smentisce la mia teoria che esistono per essere chiamato da altre lingue. Ovviamente puoi equiparare due numeri interi in altre lingue senza questi operatori, quindi torniamo alla domanda "perché esistono per double"?


12
L'unico problema che posso vedere con questo è che la specifica del linguaggio C # dice che gli operatori sovraccarichi hanno la precedenza sugli operatori integrati. Quindi sicuramente, un compilatore C # conforme dovrebbe vedere che un operatore sovraccarico è disponibile qui e generare la ricorsione infinita. Hmm. Preoccupante.
Damien_The_Unbeliever,

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 del sovraccarico dell'operatore binario della specifica del linguaggio C #, mi aspetterei anche una ricorsione infinita. Suppongo che la fonte di riferimento ( riferimentiource.microsoft.com/#mscorlib/system/… ) non si applichi qui.
Dirk Vollmar,

6
@DStanley - Non sto negando ciò che viene prodotto. Sto dicendo che non posso riconciliarlo con le specifiche della lingua. Questo è ciò che preoccupa. Stavo pensando di approfondire Roslyn e vedere se potevo trovare qualche trattamento speciale qui, ma al momento non sono pronto per farlo (macchina sbagliata)
Damien_The_Unbeliever

1
@Damien_The_Unbeliever Ecco perché penso che sia un'eccezione alle specifiche o una diversa interpretazione degli operatori "integrati".
D Stanley,

1
Dato che @Jon Skeet non ha ancora risposto o commentato questo, sospetto che sia un bug (ovvero una violazione delle specifiche).
TheBlastOne

37

La principale confusione qui è che stai assumendo che tutte le librerie .NET (in questo caso, la Libreria numerica estesa, che non fa parte del BCL) siano scritte in C # standard. Questo non è sempre il caso e lingue diverse hanno regole diverse.

In C # standard, il pezzo di codice che si vede comporterebbe un overflow dello stack, a causa del modo in cui funziona la risoluzione di sovraccarico dell'operatore. Tuttavia, il codice non è in realtà in C # standard - utilizza fondamentalmente 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 codice C # equivalente al 100% - questo semplicemente non è possibile in C # con il tuo 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 op_Equalitychiamata con il semplice ceq. Ancora una volta, non puoi replicarlo nella tua DoubleExstruttura: è la magia del compilatore.

Questa non è certamente una situazione unica in .NET - c'è un sacco di codice che non è valido, C # standard. I motivi sono di solito (a) hack del compilatore e (b) un linguaggio diverso, con gli hack dispari (c) di runtime (ti sto guardando Nullable!!).

Poiché il compilatore Roslyn C # è oepn source, posso effettivamente indicarti il ​​punto in cui viene decisa la risoluzione del sovraccarico:

Il luogo in cui vengono risolti tutti gli operatori binari

Le "scorciatoie" per operatori intrinseci

Quando guardi le scorciatoie, vedrai che l'uguaglianza tra doppio e doppio risulta nell'operatore intrinseco doppio, mai nell'operatore effettivo ==definito sul tipo. Il sistema di tipi .NET deve fingere che Doublesia un tipo come un altro, ma C # no - doubleè una primitiva in C #.


1
Non sono sicuro di essere d'accordo sul fatto che il codice nella fonte di riferimento sia semplicemente "decodificato". Il codice ha direttive del compilatore #ife altri artefatti che non sarebbero presenti nel codice compilato. Inoltre, se è stato progettato per il contrario, doubleperché non è stato progettato per il contrario into long? Penso che ci sia una ragione per il codice sorgente, ma credo che l'uso di ==all'interno dell'operatore venga compilato in un modo CEQche impedisce la ricorsione. Poiché l'operatore è un operatore "predefinito" per quel tipo (e non può essere ignorato), le regole di sovraccarico non si applicano.
D Stanley,

@DStanley Non volevo sottintendere che tutto il codice fosse decodificato. E ancora, doublenon fa parte del BCL - è in una libreria separata, che sembra essere inclusa nella specifica C #. Sì, ==viene compilato in a ceq, ma ciò significa comunque che si tratta di un hack del compilatore che non è possibile replicare nel proprio codice e qualcosa che non fa parte della specifica C # (proprio come il float64campo sulla Doublestruttura). Non è una parte contrattuale di C #, quindi non ha senso trattarlo come C # valido, anche se è stato compilato con il compilatore C #.
Luaan,

@Danamente 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 ++. Naturalmente c'è ancora molto codice nativo .NET, ma cose come "confrontare due doppi" non funzionerebbero davvero bene in .NET puro; 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 citato in precedenza - per assicurarsi che altri compilatori .NET possano trattare quei tipi come tipi .NET reali.
Luaan,

@DStanley Ma va bene, punto preso. Ho rimosso il riferimento "reverse engineering" e ho riformulato la risposta per menzionare esplicitamente "C # standard", anziché solo C #. E non trattare doubleallo stesso modo di inte long- inte longsono tipi primitivi che devono supportare tutti i linguaggi .NET. float, decimalE doublenon lo sono.
Luaan,

12

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

Normalmente non è possibile definire una struttura ricorsiva come questa:

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

I tipi primitivi hanno il loro supporto nativo anche in CIL. Normalmente non vengono trattati come tipi orientati agli oggetti. Un doppio è solo un valore a 64 bit se viene utilizzato come float64in 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 utilizza direttamente il tipo doppio tipo, non verrà mai chiamato. A proposito, la sua fonte è simile a questa 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 puoi vedere, non esiste un loop infinito (lo ceqstrumento viene utilizzato invece di chiamare il System.Double::op_Equality). Quindi quando un doppio viene trattato come un oggetto, verrà chiamato il metodo dell'operatore, che alla fine lo gestirà come il float64tipo primitivo 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), provare il codice public struct MyNumber { internal MyNumber m_value; }. Non può essere compilato, ovviamente. L'errore è errore CS0523: Il membro Struct 'MyNumber.m_value' di tipo 'MyNumber' provoca un ciclo nel layout della struttura
Jeppe Stig Nielsen

8

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

Ero curioso di vedere se il compilatore C # avrebbe fatto riferimento ceqo l' ==operatore quando avrebbe confrontato due valori doppi. Nell'esempio banale che ho trovato (sotto), ha usatoceq .

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 (notare l'istruzione con l'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

-2

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


Cosa System.Runtime.Versioningc'entra System.Double?
Koopakiller,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.