La conversión de un número decimal a doble en C # da como resultado una diferencia

Resumen del problema:

Para algunos valores decimales, cuando convertimos el tipo de decimal a doble, se agrega una pequeña fracción al resultado.

Lo que lo empeora es que puede haber dos valores decimales “iguales” que dan como resultado valores dobles diferentes al convertirlos.

Muestra de código:

decimal dcm = 8224055000.0000000000m; // dcm = 8224055000 double dbl = Convert.ToDouble(dcm); // dbl = 8224055000.000001 decimal dcm2 = Convert.ToDecimal(dbl); // dcm2 = 8224055000 double dbl2 = Convert.ToDouble(dcm2); // dbl2 = 8224055000.0 decimal deltaDcm = dcm2 - dcm; // deltaDcm = 0 double deltaDbl = dbl2 - dbl; // deltaDbl = -0.00000095367431640625 

Mira los resultados en los comentarios. Los resultados se copian del reloj del depurador. Los números que producen este efecto tienen muchos menos dígitos decimales que el límite de los tipos de datos, por lo que no puede ser un desbordamiento (¡supongo!).

Lo que lo hace mucho más interesante es que puede haber dos valores decimales iguales (en el ejemplo de código anterior, vea “dcm” y “dcm2”, con “deltaDcm” igual a cero) dando como resultado valores dobles diferentes al convertirlos. (En el código, “dbl” y “dbl2”, que tienen un “deltaDbl” distinto de cero)

Supongo que debería ser algo relacionado con la diferencia en la representación bit a bit de los números en los dos tipos de datos, ¡pero no puedo entender qué! Y necesito saber qué hacer para que la conversión sea como yo la necesito. (como dcm2 -> dbl2)

Interesante, aunque generalmente no confío en las formas normales de escribir valores de coma flotante cuando está interesado en los resultados exactos.

Aquí hay una demostración un poco más simple, usando DoubleConverter.cs que he usado varias veces antes.

 using System; class Test { static void Main() { decimal dcm1 = 8224055000.0000000000m; decimal dcm2 = 8224055000m; double dbl1 = (double) dcm1; double dbl2 = (double) dcm2; Console.WriteLine(DoubleConverter.ToExactString(dbl1)); Console.WriteLine(DoubleConverter.ToExactString(dbl2)); } } 

Resultados:

 8224055000.00000095367431640625 8224055000 

Ahora la pregunta es por qué el valor original (8224055000.0000000000) que es un número entero -y exactamente representable como un double termina con datos adicionales en. Sospecho fuertemente que se debe a peculiaridades en el algoritmo utilizado para convertir de decimal a double , pero es desgraciado.

También viola la sección 6.2.1 de la especificación C #:

Para una conversión de decimal a flotante o doble, el valor decimal se redondea al valor double o float más cercano. Si bien esta conversión puede perder precisión, nunca provoca una excepción.

El “valor doble más cercano” es claramente 8224055000 … así que este es un error de OMI. Sin embargo, no es uno que esperara solucionarse pronto. (Por cierto, da los mismos resultados en .NET 4.0b1.)

Para evitar el error, es probable que desee normalizar el valor decimal primero, efectivamente “eliminando” los 0 adicionales después del punto decimal. Esto es algo complicado ya que involucra una aritmética de enteros de 96 bits: la clase .NET 4.0 BigInteger bien puede hacer que sea más fácil, pero puede que esa no sea una opción para usted.

La respuesta radica en el hecho de que los decimal intentan preservar el número de dígitos significativos. Por lo tanto, 8224055000.0000000000m tiene 20 dígitos significativos y se almacena como 82240550000000000000E-10 , mientras que 8224055000m tiene solo 10 y se almacena como 8224055000E+0 . la mantisa double es (lógicamente) 53 bits, es decir, 16 dígitos decimales como máximo. Esta es exactamente la precisión que obtienes al convertir al double , y de hecho, el callejero 1 en tu ejemplo está en el decimosexto lugar decimal. La conversión no es de 1 a 1 porque el double usa la base 2.

Estas son las representaciones binarias de sus números:

 dcm: 00000000000010100000000000000000 00000000000000000000000000000100 01110101010100010010000001111110 11110010110000000110000000000000 dbl: 0.10000011111.1110101000110001000111101101100000000000000000000001 dcm2: 00000000000000000000000000000000 00000000000000000000000000000000 00000000000000000000000000000001 11101010001100010001111011011000 dbl2 (8224055000.0): 0.10000011111.1110101000110001000111101101100000000000000000000000 

Para el doble, utilicé puntos para delimitar los campos de signo, exponente y mantisa; para decimal, vea MSDN en decimal . GetBits , pero esencialmente los últimos 96 bits son la mantisa. Observe cómo los bits de mantisa de dcm2 y los bits más significativos de dbl2 coinciden exactamente (no se olvide de los 1 bits implícitos en la mantisa double ), y de hecho estos bits representan 8224055000. Los bits de mantisa de dbl son los mismos que en dcm2 y dbl2 pero para el desagradable 1 en el bit menos significativo. El exponente de dcm es 10, y la mantisa es 82240550000000000000.

Actualización II: en realidad es muy fácil quitar ceros al final.

 // There are 28 trailing zeros in this constant — // no decimal can have more than 28 trailing zeros const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000m ; // decimal.ToString() faithfully prints trailing zeroes Assert ((8224055000.000000000m).ToString () == "8224055000.000000000") ; // Let System.Decimal.Divide() do all the work Assert ((8224055000.000000000m / PreciseOne).ToString () == "8224055000") ; Assert ((8224055000.000010000m / PreciseOne).ToString () == "8224055000.00001") ; 

El artículo Lo que todo científico informático debería saber sobre la aritmética de coma flotante sería un excelente lugar para comenzar.

La respuesta corta es que la aritmética binaria en coma flotante es necesariamente una aproximación , y no siempre es la aproximación que usted adivinaría. Esto se debe a que las CPU hacen aritmética en la base 2, mientras que los humanos (generalmente) hacen aritmética en la base 10. Hay una gran variedad de efectos inesperados que se derivan de esto.

Para ver este problema más claramente ilustrado, intente esto en LinqPad (o reemplace todos los .Dump () y cambie a Console.WriteLine () s si le parece).

Me parece lógicamente incorrecto que la precisión del decimal pueda dar como resultado 3 dobles diferentes. Felicitaciones a @AntonTykhyy por la idea / PreciseOne:

 ((double)200M).ToString("R").Dump(); // 200 ((double)200.0M).ToString("R").Dump(); // 200 ((double)200.00M).ToString("R").Dump(); // 200 ((double)200.000M).ToString("R").Dump(); // 200 ((double)200.0000M).ToString("R").Dump(); // 200 ((double)200.00000M).ToString("R").Dump(); // 200 ((double)200.000000M).ToString("R").Dump(); // 200 ((double)200.0000000M).ToString("R").Dump(); // 200 ((double)200.00000000M).ToString("R").Dump(); // 200 ((double)200.000000000M).ToString("R").Dump(); // 200 ((double)200.0000000000M).ToString("R").Dump(); // 200 ((double)200.00000000000M).ToString("R").Dump(); // 200 ((double)200.000000000000M).ToString("R").Dump(); // 200 ((double)200.0000000000000M).ToString("R").Dump(); // 200 ((double)200.00000000000000M).ToString("R").Dump(); // 200 ((double)200.000000000000000M).ToString("R").Dump(); // 200 ((double)200.0000000000000000M).ToString("R").Dump(); // 200 ((double)200.00000000000000000M).ToString("R").Dump(); // 200 ((double)200.000000000000000000M).ToString("R").Dump(); // 200 ((double)200.0000000000000000000M).ToString("R").Dump(); // 200 ((double)200.00000000000000000000M).ToString("R").Dump(); // 200 ((double)200.000000000000000000000M).ToString("R").Dump(); // 199.99999999999997 ((double)200.0000000000000000000000M).ToString("R").Dump(); // 200 ((double)200.00000000000000000000000M).ToString("R").Dump(); // 200.00000000000003 ((double)200.000000000000000000000000M).ToString("R").Dump(); // 200 ((double)200.0000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997 ((double)200.00000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997 "\nFixed\n".Dump(); const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000M; ((double)(200M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.0000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 ((double)(200.00000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200 

Este es un viejo problema, y ​​ha sido el tema de muchas preguntas similares en StackOverflow.

La explicación simplista es que los números decimales no se pueden representar exactamente en binario

Este enlace es un artículo que podría explicar el problema.