c# - Definición de operador "==" para Doble
.net language-lawyer (5)
Por alguna razón, me estaba infiltrando en la fuente de .NET Framework para la clase
Double
y 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?
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.
Eché un vistazo al
CIL
con JustDecompile.
El
==
interno se traduce al código de
ceq
CIL
ceq
.
En otras palabras, es la igualdad primitiva de CLR.
Tenía curiosidad por ver si el compilador de C # haría referencia a
ceq
o al operador
==
al comparar dos valores dobles.
En el ejemplo trivial que se me ocurrió (a continuación), usó
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
En realidad, el compilador convertirá el operador
==
en un código
ceq
IL, y no se llamará al operador que mencione.
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 a una llamada
CEQ
directamente (o mediante reflexión).
El código
dentro
del operador
se
compilará en un
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 instrucción
CEQ
), 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, los mismos operadores NO existen (ya sea en la fuente de referencia o mediante reflexión) para los tipos integrales, solo
Single
,
Double
,
Decimal
,
String
y
DateTime
, lo que refuta mi teoría de que existen para ser llamados desde otros idiomas.
Obviamente, puede equiparar dos enteros en otros idiomas sin estos operadores, así que volvemos a la pregunta "¿por qué existen para el
double
"?
La fuente de los tipos primitivos puede ser confusa.
¿Has visto la primera línea de la estructura
Double
?
Normalmente no puedes 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
float64
en 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 instrumento
ceq
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 tipo primitivo
float64
en el nivel CIL.
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 llamada
op_Equality
con solo el simple
ceq
.
De nuevo, no puede replicar esto en su propia estructura
DoubleEx
: 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 observa los accesos directos, verá que la igualdad entre doble y doble da como resultado el operador intrínseco doble,
nunca
en el operador
==
real definido en el tipo.
El sistema de tipos .NET tiene que fingir que
Double
es un tipo como cualquier otro, pero C # no:
double
es una primitiva en C #.