c# - Conversión implícita a System.Double con una estructura anulable a través de locales generados por el compilador: ¿por qué falla esto?
nullable cil (2)
Dado lo siguiente, ¿por qué se lanza la excepción InvalidCastException? No puedo ver por qué debería estar fuera de un error (esto está en x86; x64 se bloquea con un 0xC0000005 en clrjit.dll).
class Program
{
static void Main(string[] args)
{
MyDouble? my = new MyDouble(1.0);
Boolean compare = my == 0.0;
}
struct MyDouble
{
Double? _value;
public MyDouble(Double value)
{
_value = value;
}
public static implicit operator Double(MyDouble value)
{
if (value._value.HasValue)
{
return value._value.Value;
}
throw new InvalidCastException("MyDouble value cannot convert to System.Double: no value present.");
}
}
}
Aquí está el CIL generado para Main()
:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 3
.locals init (
[0] valuetype [mscorlib]System.Nullable`1<valuetype Program/MyDouble> my,
[1] bool compare,
[2] valuetype [mscorlib]System.Nullable`1<valuetype Program/MyDouble> CS$0$0000,
[3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0001)
L_0000: nop
L_0001: ldloca.s my
L_0003: ldc.r8 1
L_000c: newobj instance void Program/MyDouble::.ctor(float64)
L_0011: call instance void [mscorlib]System.Nullable`1<valuetype Program/MyDouble>::.ctor(!0)
L_0016: nop
L_0017: ldloc.0
L_0018: stloc.2
L_0019: ldloca.s CS$0$0000
L_001b: call instance bool [mscorlib]System.Nullable`1<valuetype Program/MyDouble>::get_HasValue()
L_0020: brtrue.s L_002d
L_0022: ldloca.s CS$0$0001
L_0024: initobj [mscorlib]System.Nullable`1<float64>
L_002a: ldloc.3
L_002b: br.s L_003e
L_002d: ldloca.s CS$0$0000
L_002f: call instance !0 [mscorlib]System.Nullable`1<valuetype Program/MyDouble>::GetValueOrDefault()
L_0034: call float64 Program/MyDouble::op_Implicit(valuetype Program/MyDouble)
L_0039: newobj instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0)
L_003e: stloc.3
L_003f: ldloca.s CS$0$0001
L_0041: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault()
L_0046: call float64 Program/MyDouble::op_Implicit(valuetype Program/MyDouble)
L_004b: conv.r8
L_004c: ldc.r8 0
L_0055: bne.un.s L_0060
L_0057: ldloca.s CS$0$0001
L_0059: call instance bool [mscorlib]System.Nullable`1<float64>::get_HasValue()
L_005e: br.s L_0061
L_0060: ldc.i4.0
L_0061: stloc.1
L_0062: ret
}
Observe las líneas 0x2D - 0x3E en la IL. ¿Recupera el MyDouble?
instancia, llama a GetValueOrDefault
en él, llama al operador implícito en eso, y luego envuelve el resultado en un Double?
y lo almacena en el CS$0$0001
local generado por el compilador. En las líneas 0x3F a 0x55, recuperamos el CS$0$0001
, ''desenvolver'' a través de GetValueOrDefault
y luego lo comparamos con 0 ... ¡ PERO ESPERE UN MINUTO ! ¿Qué hace esa llamada adicional a MyDouble::op_Implicit
en la línea 0x46?
Si depuramos el programa C #, de hecho vemos 2 llamadas al implicit operator Double(MyDouble value)
, y es la segunda llamada la que falla, ya que el value
no se inicializa.
¿Que esta pasando aqui?
Es claramente un error del compilador de C #. Gracias por llamar mi atención.
Incidentalmente, es una mala práctica tener un operador de conversión implícito definido por el usuario que lanza una excepción; La documentación establece que las conversiones implícitas deben ser aquellas que nunca se lanzan. ¿Estás seguro de que no quieres que esto sea una conversión explícita?
De todos modos, volvamos al error.
El error se reprosca en C # 3 y 4 pero no en C # 2. Lo que significa que fue mi culpa. Probablemente causé el error cuando volví a hacer el código de operador implícito levantado definido por el usuario para que funcione con el árbol de expresiones lambdas. ¡Lo siento por eso! Ese código es muy complicado, y al parecer no lo probé adecuadamente.
Lo que se supone que hace el código es:
Primero, la resolución de sobrecarga intenta resolver el significado de ==. El mejor operador == para el que ambos argumentos son válidos es el operador levantado que compara dos dobles anulables. Por lo tanto, debe ser analizado como:
Boolean compare = (double?)my == (double?)0.0;
(Si escribe el código como este, entonces hace lo correcto en C # 3 y 4.)
El significado del operador levantado == es:
- evaluar ambos argumentos
- Si ambos son nulos, entonces el resultado es verdadero. Claramente, esto no puede ocurrir en este caso.
- Si uno es nulo y el otro no, el resultado es falso.
- si ambos no son nulos, ambos se desenvuelven para duplicarse y se comparan como dobles.
Ahora la pregunta es "¿cuál es la forma correcta de evaluar el lado izquierdo?"
Tenemos aquí un operador de conversión levantado definido por el usuario de MyDouble? ¿para duplicar?. El comportamiento correcto es:
- Si "mi" es nulo, entonces el resultado es un doble nulo?
- Si "mi" no es nulo, el resultado es la conversión definida por el usuario de my.Value to double, y luego la conversión de ese double to double?
Claramente algo va mal en este proceso.
Ingresaré un error en nuestra base de datos, pero cualquier solución probablemente perderá la fecha límite para los cambios que llegan al próximo service pack. Estaría buscando soluciones si fuera usted. Una vez más, disculpas por el error.
Esto seguro que me parece un error de compilación para mí. La IL sugiere que el compilador está generando código para convertir MyDouble? ¿A un doble con el operador de conversión, luego a un doble? Pero cae en picado cuando luego usa el operador de conversión de nuevo en ese doble? Eso es malo, tipo de argumento equivocado. Tampoco hay una necesidad.
Este artículo de retroalimentación se parece a este error. Ya con más de 6 años, eso debe ser una parte difícil del compilador. Me imagino que es.