Errores graves con conversiones levantadas / nullables desde int, permitiendo la conversión desde decimal


Creo que esta pregunta me traerá fama instantánea aquí en Stack Overflow.

Supongamos que tiene el siguiente tipo:

// represents a decimal number with at most two decimal places after the period
struct NumberFixedPoint2
{
    decimal number;

    // an integer has no fractional part; can convert to this type
    public static implicit operator NumberFixedPoint2(int integer)
    {
        return new NumberFixedPoint2 { number = integer };
    }

    // this type is a decimal number; can convert to System.Decimal
    public static implicit operator decimal(NumberFixedPoint2 nfp2)
    {
        return nfp2.number;
    }

    /* will add more nice members later */
}

Se ha escrito de tal manera que solo se permiten conversiones seguras que no pierden precisión. Sin embargo, cuando pruebo este código:

    static void Main()
    {
        decimal bad = 2.718281828m;
        NumberFixedPoint2 badNfp2 = (NumberFixedPoint2)bad;
        Console.WriteLine(badNfp2);
    }

Me sorprende esto compila y, cuando se ejecuta, escribe 2. La conversión de int (de valor 2) a NumberFixedPoint2 es importante aquí. (Se prefiere una sobrecarga de WriteLine que toma un System.Decimal, en caso de que alguien se pregunte.)

¿Por qué en la Tierra se permite la conversión de decimal a NumberFixedPoint2? (Por cierto, en el código anterior, si NumberFixedPoint2 se cambia de una estructura a una clase, nada cambia.)

¿Sabe si la Especificación del lenguaje C# dice que una conversión implícita de int a un tipo personalizado "implica" la existencia de una conversión explícita "directa" de decimal a ese tipo personalizado?

Se vuelve mucho peor. Pruebe este código en su lugar:

    static void Main()
    {
        decimal? moreBad = 7.3890560989m;
        NumberFixedPoint2? moreBadNfp2 = (NumberFixedPoint2?)moreBad;
        Console.WriteLine(moreBadNfp2.Value);
    }

Como ves, nosotros tienen (levantadas) Nullable<> conversiones aquí. Pero sí, eso compila.

Cuando se compila en x86 "platform", este código escribe un valor numérico impredecible. Cuál varía de vez en cuando. Como ejemplo, en una ocasión tuve 2289956. ¡Ese es un error serio!

Cuando se compila para la plataforma x64, el código anterior bloquea la aplicación con un System.InvalidProgramException con el mensaje Common Language Runtime detectó un programa no válido. Según la documentación de la clase InvalidProgramException:

Generalmente esto indica un error en el compilador que generó el programa.

¿Alguien (como Eric Lippert, o alguien que ha trabajado con conversiones elevadas en el compilador de C#) sabe la causa de estos errores? Como, ¿cuál es una condición suficiente para que no nos topemos con ellos en nuestro código? Porque el tipo NumberFixedPoint2 es en realidad algo que tenemos en código real (administrar el dinero de otras personas y cosa).

Author: Jeppe Stig Nielsen, 2013-08-20

2 answers

Su segunda parte (usando tipos nullables) parece ser muy similar a este error conocido en el compilador actual. De la respuesta sobre el problema Connect:

Si bien actualmente no tenemos planes para abordar este problema en la próxima versión de Visual Studio, planeamos investigar una solución en Roslyn

Como tal, esperamos que este error se corrija en una versión futura de Visual Studio y los compiladores.

 24
Author: Reed Copsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-20 19:07:41

Solo estoy respondiendo a la primera parte de la pregunta para empezar. (Sugiero que la segunda parte debe ser una pregunta separada; es más probable que sea un error.)

Solo hay una explícita conversión de decimal a int, pero esa conversión está siendo implícitamente llamada en tu código. La conversión sucede en este IL:

IL_0010:  stloc.0
IL_0011:  ldloc.0
IL_0012:  call       int32 [mscorlib]System.Decimal::op_Explicit(valuetype [mscorlib]System.Decimal)
IL_0017:  call       valuetype NumberFixedPoint2 NumberFixedPoint2::op_Implicit(int32)

Creo que este es el comportamiento correcto de acuerdo con la especificación, aunque es sorprendente1. Vamos a trabajar nuestro camino a través de la sección 6.4.5 de la especificación de C# 4 (Conversiones Explícitas definidas por el usuario). No voy a copiar todo el texto, ya que sería tedioso, solo cuáles son los resultados relevantes en nuestro caso. Del mismo modo, no voy a usar subíndices, ya que no funcionan bien con la fuente de código aquí:)

  • Determinar los tipos S0 y T0: S0 es decimal, y T0 es NumberFixedPoint2.
  • Encuentre el conjunto de tipos, D, a partir del cual se considerarán los operadores de conversión definidos como usados: solo { decimal, NumberFixedPoint2 }
  • Encuentre el conjunto de operadores de conversión definidos por el usuario y elevados aplicables, U. decimal abarca int (sección 6.4.3) porque hay una conversión implícita estándar de int a decimal. Así que el operador de conversión explícito es en U, y es de hecho el único miembro de U
  • Encuentre el tipo de fuente más específico, Sx, de los operadores en U
    • El operador no convierte desde S (decimal) así que la primera bala es out
    • El operador no convierte desde un tipo que abarca S (decimal encompasses int, not the other way round) so the second bullet is out
    • Eso solo deja la tercera bala, que habla del "tipo más abarcador" - bueno, solo tenemos un tipo, así que está bien: Sx es int.
  • Encuentre el tipo de destino más específico, Tx, de los operadores en U
    • El operador se convierte directamente a NumberFixedPoint2 por lo que Tx es NumberFixedPoint2.
  • Encuentre el operador de conversión más específico:
    • U contiene exactamente un operador, que de hecho convierte de Sx a Tx, por lo que es el operador más específico
  • Finalmente, aplicar la conversión:
    • Si S no es Sx, entonces se realiza una conversión explícita estándar de S a Sx. (Así que es decimal a int.)
    • Se invoca el operador de conversión definido por el usuario más específico (su operador)
    • T es Tx así que no hay necesidad de la conversión en la tercera bala

La línea en negrita es el bit que confirma que una conversión explícita estándar es realmente factible, cuando solo se especifica una conversión explícita de un tipo diferente.


1 Bueno, lo encontré sorprendente, al menos. No soy consciente de haber visto esto antes.

 44
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-20 19:15:11