Definición de operador "==" para Doble

126

Por alguna razón, me estaba infiltrando en la fuente de .NET Framework para la clase Doubley descubrí que la declaración de ==es:

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

La misma lógica se aplica a todos los operadores.


  • ¿Cuál es el punto de tal definición?
  • ¿Como funciona?
  • ¿Por qué no crea una recursión infinita?
Thomas Ayoub
fuente
17
Esperaría una recursión sin fin.
HimBromBeere 01 de
55
Estoy bastante seguro de que no se usa para comparar en ninguna parte con el doble, sino que ceqse emite en IL. Esto solo está ahí para llenar algún propósito de documentación, aunque no puedo encontrar la fuente.
Habib
2
Lo más probable es que este operador se pueda obtener a través de Reflection.
Damien_The_Unbeliever 01 de
3
Eso nunca se llamará, el compilador tiene la lógica de igualdad incorporada (ceq opcode) ver ¿ Cuándo se invoca el operador Double ==?
Alex K.
1
@ZoharPeled dividir un doble con cero es válido y dará como resultado un infinito positivo o negativo.
Magnus

Respuestas:

62

En realidad, el compilador convertirá al ==operador en un ceqcódigo IL, y no se llamará al operador que menciona.

Es probable que el motivo del operador en el código fuente sea para que pueda llamarse desde lenguajes distintos de C # que no lo traducen CEQdirectamente a una llamada (o mediante reflexión). El código dentro del operador se compilará en a CEQ, por lo que no hay recursión infinita.

De hecho, si llama al operador a través de la reflexión, puede ver que se llama al operador (en lugar de una CEQinstrucción), y obviamente no es infinitamente recursivo (ya que el programa termina como se esperaba):

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 resultante (compilado por 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 

Curiosamente - NO existen los mismos operadores (ya sea en la fuente de referencia o por medio de la reflexión) para los tipos integrales, solamente Single, Double, Decimal, String, y DateTime, lo que refuta mi teoría de que existen a ser llamado de otros idiomas. Obviamente, puede equiparar dos enteros en otros idiomas sin estos operadores, así que volvemos a la pregunta "¿por qué existen para double"?

D Stanley
fuente
12
El único problema que puedo ver con esto es que la especificación del lenguaje C # dice que los operadores sobrecargados tienen prioridad sobre los operadores integrados. Por lo tanto, un compilador C # conforme debería ver que un operador sobrecargado está disponible aquí y generar la recursión infinita. Hmm Problemas
Damien_The_Unbeliever 01 de
55
Eso no responde la pregunta, en mi humilde opinión. Solo explica a qué se traduce el código, pero no por qué. De acuerdo con la sección 7.3.4 Resolución de sobrecarga del operador binario de la especificación del lenguaje C # También esperaría una recursión infinita. Supongo que la fuente de referencia ( referencesource.microsoft.com/#mscorlib/system/… ) realmente no se aplica aquí.
Dirk Vollmar
66
@DStanley - No estoy negando lo que se produce. Estoy diciendo que no puedo conciliarlo con la especificación del idioma. Eso es lo que preocupa. Estaba pensando en estudiar detenidamente a Roslyn y ver si podía encontrar algún manejo especial aquí, pero no estoy preparado para hacerlo en este momento (máquina equivocada)
Damien_The_Unbeliever
1
@Damien_The_Unbeliever Es por eso que creo que es una excepción a la especificación o una interpretación diferente de los operadores "incorporados".
D Stanley
1
Como @Jon Skeet aún no respondió o comentó sobre esto, sospecho que es un error (es decir, una violación de las especificaciones).
TheBlastOne
37

La principal confusión aquí es que está asumiendo que todas las bibliotecas .NET (en este caso, la Biblioteca de Numéricos Extendida, que no es parte del BCL) están escritas en C # estándar. Este no es siempre el caso, y diferentes idiomas tienen diferentes reglas.

En C # estándar, el fragmento de código que está viendo provocaría un desbordamiento de la pila, debido a la forma en que funciona la resolución de sobrecarga del operador. Sin embargo, el código no está realmente en C # estándar, básicamente usa características no documentadas del compilador de C #. En lugar de llamar al operador, emite este código:

ldarg.0
ldarg.1
ceq
ret

Eso es todo :) No hay un código C # 100% equivalente, esto simplemente no es posible en C # con su propio tipo.

Incluso entonces, el operador real no se usa al compilar código C #: el compilador realiza un montón de optimizaciones, como en este caso, donde reemplaza la op_Equalityllamada con solo lo simple ceq. Una vez más, no puede replicar esto en su propia DoubleExestructura: es la magia del compilador.

Ciertamente, esta no es una situación única en .NET: hay mucho código que no es válido, estándar C #. Las razones son generalmente (a) hacks de compilación y (b) un lenguaje diferente, con los hacks de tiempo de ejecución (c) extraños (¡te estoy mirando Nullable!).

Dado que el compilador Roslyn C # es una fuente abierta, puedo señalarle el lugar donde se decide la resolución de sobrecarga:

El lugar donde se resuelven todos los operadores binarios.

Los "atajos" para operadores intrínsecos

Cuando observe los accesos directos, verá que la igualdad entre doble y doble da como resultado el operador doble intrínseco, nunca en el ==operador real definido en el tipo. El sistema de tipos .NET tiene que fingir que Doublees un tipo como cualquier otro, pero C # no, doublees una primitiva en C #.

Luaan
fuente
1
No estoy seguro de estar de acuerdo en que el código en la fuente de referencia es solo "ingeniería inversa". El código tiene directivas del compilador #ify otros artefactos que no estarían presentes en el código compilado. Además, si se realizó la ingeniería inversa, ¿por doublequé no se realizó la ingeniería inversa into long? Creo que hay una razón para el código fuente, pero creo que el uso de ==dentro del operador se compila en un CEQque evita la recurrencia. Dado que el operador es un operador "predefinido" para ese tipo (y no se puede anular), las reglas de sobrecarga no se aplican.
D Stanley
@DStanley No quería dar a entender que todo el código tiene ingeniería inversa. Y nuevamente, doubleno es parte de BCL: está en una biblioteca separada, que simplemente está incluida en la especificación de C #. Sí, ==se compila en a ceq, pero eso todavía significa que este es un truco del compilador que no puede replicar en su propio código, y algo que no forma parte de la especificación de C # (al igual que el float64campo en la Doubleestructura). No es una parte contractual de C #, por lo que tiene poco sentido tratarlo como un C # válido, incluso si se compiló con el compilador de C #.
Luaan
@DStanely No pude encontrar cómo está organizado el marco real, pero en la implementación de referencia de .NET 2.0, todas las partes difíciles son solo intrínsecas del compilador, implementadas en C ++. Todavía hay mucho código nativo .NET, por supuesto, pero cosas como "comparar dos dobles" realmente no funcionarían bien en .NET puro; esa es una de las razones por las que los números de coma flotante no se incluyen en el BCL. Dicho esto, el código también se implementa en C # (no estándar), probablemente exactamente por la razón que mencionó anteriormente, para asegurarse de que otros compiladores .NET puedan tratar esos tipos como tipos reales .NET.
Luaan
@DStanley Pero está bien, punto tomado. Eliminé la referencia de "ingeniería inversa" y reformulé la respuesta para mencionar explícitamente "C # estándar", en lugar de solo C #. Y no trate de doublela misma manera que inty long, inty longson tipos primitivos que todos los lenguajes .NET deben admitir. float, decimalY doubleno lo son.
Luaan
12

La fuente de los tipos primitivos puede ser confusa. ¿Has visto la primera línea de la Doubleestructura?

Normalmente no puede definir una estructura recursiva como esta:

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

Los tipos primitivos también tienen su soporte nativo en CIL. Normalmente no se tratan como tipos orientados a objetos. Un doble es solo un valor de 64 bits si se usa como float64en CIL. Sin embargo, si se maneja como un tipo .NET habitual, contiene un valor real y contiene métodos como cualquier otro tipo.

Entonces, lo que ves aquí es la misma situación para los operadores. Normalmente, si utiliza el tipo de tipo doble directamente, nunca se llamará. Por cierto, su fuente se ve así en 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
}

Como puede ver, no hay un bucle sin fin (el ceqinstrumento se usa en lugar de llamar al System.Double::op_Equality). Entonces, cuando un doble se trata como un objeto, se llamará al método del operador, que eventualmente lo manejará como el float64tipo primitivo en el nivel CIL.

György Kőszeg
fuente
1
Para aquellos que no entienden la primera parte de esta publicación (tal vez porque generalmente no escriben sus propios tipos de valor), intente el código public struct MyNumber { internal MyNumber m_value; }. No se puede compilar, por supuesto. El error es el error CS0523: el miembro de estructura 'MyNumber.m_value' del tipo 'MyNumber' provoca un ciclo en el diseño de estructura
Jeppe Stig Nielsen
8

Eché un vistazo al CIL con JustDecompile. El interno ==se traduce al código operativo CIL ceq . En otras palabras, es la igualdad primitiva de CLR.

Tenía curiosidad por ver si el compilador de C # haría referencia ceqo el ==operador al comparar dos valores dobles. En el ejemplo trivial que se me ocurrió (a continuación), se utilizó ceq.

Este programa:

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 el siguiente CIL (tenga en cuenta la declaración con la etiqueta 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
fuente
-2

Como se indica en la documentación de Microsoft para el espacio de nombres System.Runtime.Versioning: los tipos que se encuentran en este espacio de nombres están destinados para su uso dentro de .NET Framework y no para aplicaciones de usuario. El espacio de nombres System.Runtime.Versioning contiene tipos avanzados que admiten el control de versiones en implementaciones en paralelo de .NET Framework.

Thomas Papamihos
fuente
¿Qué tiene System.Runtime.Versioningque ver con System.Double?
Koopakiller