在C#中将小数转换为双精度数会导致差异
问题总结:
对于一些十进制值,当我们将types从十进制转换为双精度时,一小部分被添加到结果中。
更糟的是,可以有两个“相等的”十进制值,这些值在转换时会导致不同的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
看看评论中的结果。 结果从debugging器的手表复制而来。 产生这种效果的数字比数据types的限制小得多,所以它不能是溢出(我想!)。
更令人感兴趣的是可以有两个相等的十进制值(在上面的代码示例中,请参见“dcm”和“dcm2”,其中“deltaDcm”等于零)导致转换后的不同 double值。 (在代码中,具有非零“deltaDbl”的“dbl”和“dbl2”)
我想这应该是在两个数据types的数字按位表示的差异,但不能弄清楚什么! 而且我需要知道该怎么做才能按照我所需要的方式进行转换。 (如dcm2 – > dbl2)
有趣的 – 虽然我一般不相信正常的方式写出浮点值,当你对确切的结果感兴趣。
这是一个稍微简单的演示,使用DoubleConverter.cs
,我曾经使用过几次。
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
表示 – 最终以额外的数据结束。我强烈怀疑这是由于用于从decimal
转换为double
的algorithm中的怪癖,但它是不幸的。
它也违反了C#规范的第6.2.1节:
对于从十进制到浮点或双精度的转换,十进制值将四舍五入为最接近的双精度值或浮点值。 虽然这种转换可能会失去精度,但不会导致抛出exception。
“最接近的双倍值”显然只是8224055000 …所以这是一个错误IMO。 这不是我期望能在短时间内得到解决的。 (它在.NET 4.0b1中给出了相同的结果。)
为了避免这个错误,你可能首先要正确化十进制值,有效地“移除”小数点后的多余的0。 这是有点棘手,因为它涉及到96位整数算术 – .NET 4.0 BigInteger
类可能会更容易,但这可能不是一个选项。
答案在于, decimal
尝试保留有效位数。 因此, 8224055000.0000000000m
有20个有效数字,存储为82240550000000000000E-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
对于双倍的,我用点来划分符号,指数和尾数字段; 对于十进制数,请参阅MSDN上的decimal.GetBits ,但实质上最后的96位是尾数。 请注意dcm2
的尾数位和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") ;
文章“每个计算机科学家应该知道什么是浮点运算”将是一个很好的起点。
简单的答案是浮点二进制算术必然是一个近似值 ,并不总是您猜测的近似值。 这是因为CPU以2为基础进行算术运算,而人类(通常)以10为基数进行算术运算。由此产生了各种各样的意外效应。
要更清楚地看到这个问题,可以在LinqPad中试试(或者如果你喜欢的话,把所有的.Dump()replace成Console.WriteLine()。
在我看来,小数的精度可能会导致3个不同的双打,这在逻辑上是不正确的。 / PreciseOne创意荣誉@AntonTykhyy:
((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
这是一个老问题,并且一直是StackOverflow上许多类似问题的主题。
简单的解释是,十进制数不能用二进制精确表示
这个链接是可以解释问题的文章。