在C#中,将十进制数转换为双精度数会产生差异

在C#中,将十进制数转换为双精度数会产生差异,c#,double,decimal,C#,Double,Decimal,问题摘要: 对于某些十进制值,当我们将类型从decimal转换为double时,会向结果中添加一小部分 更糟糕的是,可能有两个“相等”的十进制值,在转换时会产生不同的双精度值 代码示例: decimal dcm = 8224055000.0000000000m; // dcm = 8224055000 double dbl = Convert.ToDouble(dcm); // dbl = 8224055000.000001 decimal dcm2 = Convert.ToDecim

问题摘要:

对于某些十进制值,当我们将类型从decimal转换为double时,会向结果中添加一小部分

更糟糕的是,可能有两个“相等”的十进制值,在转换时会产生不同的双精度值

代码示例:

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
查看评论中的结果。结果从调试器的监视中复制。 产生这种效果的数字的小数位数远远少于数据类型的限制,所以它不可能是溢出(我猜!)

更有趣的是,可以有两个相等的十进制值(在上面的代码示例中,请参见“dcm”和“dcm2”,其中“deltaDcm”等于零),从而在转换时产生不同的双精度值。(在代码中,“dbl”和“dbl2”具有非零“deltaDbl”)

我想这应该与这两种数据类型中数字的按位表示不同有关,但我不知道是什么!我需要知道怎样做才能使转换成为我需要的样子。(如dcm2->dbl2)

这篇文章将是一个很好的起点


简单的回答是,浮点二进制算法必然是一种近似值,而且它并不总是你所猜测的近似值。这是因为CPU在以基数2计算,而人类(通常)在以基数10计算。由此产生了各种各样的意想不到的影响。

这是一个老问题,也是许多关于堆栈溢出的类似问题的主题

简单的解释是十进制数不能用二进制精确表示


是一篇可能解释这个问题的文章。

很有意思-尽管我通常不相信当您对确切的结果感兴趣时写出浮点值的常规方法

这里有一个稍微简单一点的演示,我已经用过几次了

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));
    }
}
结果:

8224055000.00000095367431640625
8224055000
现在的问题是,为什么原始值(8224055000.0000000000)是一个整数,可以精确地表示为一个
double
,最终会在中包含额外的数据。我强烈怀疑这是由于用于从
十进制
转换为
双精度
的算法中的怪癖造成的,但这是不幸的

它还违反了C规范第6.2.1节:

对于从十进制到浮点或双精度的转换,十进制值四舍五入到 最接近的双精度或浮点值。虽然此转换可能会失去精度,但不会导致 要抛出的异常

“最近的双倍值”显然只有8224055000。。。所以这是一个bug,我不希望很快就能修复。(顺便说一下,它在.NET 4.0b1中给出了相同的结果。)


为了避免该错误,您可能希望首先规范化十进制值,有效地“删除”小数点后多余的0。这有点棘手,因为它涉及到96位整数运算,.NET 4.0
biginger
类可能会使它更简单,但这可能不是您的选择。

答案在于
十进制
试图保留有效位数。因此,
8224055000.0000000000 m
有20个有效数字,存储为
82240550000000e-10
,而
8224055000m
只有10个,存储为
8224055000E+0
double
的尾数(逻辑上)为53位,即最多16位十进制数字。这正是转换为
double
时所获得的精度,实际上,示例中的偏移
1
位于小数点后16位。转换不是1对1,因为
double
使用基数2

以下是您的数字的二进制表示:

dcm:
00000000000010100000000000000000 00000000000000000000000000000100
01110101010100010010000001111110 11110010110000000110000000000000
dbl:
0.10000011111.1110101000110001000111101101100000000000000000000001
dcm2:
00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000001 11101010001100010001111011011000
dbl2 (8224055000.0):
0.10000011111.1110101000110001000111101101100000000000000000000000
对于double,我使用点来分隔符号、指数和尾数字段;对于十进制,请参见,但基本上最后96位是尾数。注意
dcm2
的尾数位和
dbl2
的最高有效位是如何精确重合的(不要忘记
double
尾数中的隐式
1
位),事实上这些位代表8224055000。
dbl
的尾数位与
dcm2
dbl2
中的尾数位相同,但对于
1
的尾数位,尾数位的最低有效位相同。
dcm
的指数为10,尾数为82240550000000000000

更新II:实际上很容易去掉后面的零

// 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") ;

要更清楚地了解这个问题,请在LinqPad中尝试此方法(或者替换所有.Dump()并更改为Console.WriteLine()

在我看来,十进制的精度可能导致3种不同的双精度,这在逻辑上是不正确的。@AntonTykhyy因其/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

事实上,这并不能解释。许多十进制数不能精确地用二进制表示,但在这种情况下,输入可以精确地用二进制表示。数据正在不必要地丢失。Jon,数据没有丢失,相反-问题在于数据被不必要地保存(来自Irchi的POV,无意冒犯)。Anton,请参阅Jon发布的规范。不必要的保留数据不应破坏转换。在16位有效数字之后,十进制值指定数字全部为“0”。为什么要在第16位四舍五入到“1”?!“0”比“1”更接近“精确”的十进制值。我不知道“应该”,不是一个标准的人-但这就是它的行为,唯一的问题是如何处理这种行为。@Jon,我在回答中强调了“简单化”一词,作为记录。感谢文章链接,这是一个很长的链接,但我会尝试阅读它。基数2算术和基数10算术是我所怀疑的,但有两点:1。十进制有28-29位有效数字,双精度有15-16位有效数字。8位有效数字足够我的号码