c# .net language-lawyer

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 #.