在C#中将小数转换为双数会产生差异

时间:2009-10-18 08:00:30

标签: c# double 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)

5 个答案:

答案 0 :(得分:49)

有趣 - 虽然当您对确切结果感兴趣时,我通常不相信写出浮点值的正常方法。

这是一个稍微简单的演示,使用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 - 最终会有额外的数据。我强烈怀疑这是由于用于转换的算法中的怪癖从decimaldouble,但这很不幸。

它也违反了C#规范的第6.2.1节:

  

对于从十进制到float或double的转换,十进制值四舍五入为   最近的double或float值。虽然这种转换可能会失去精确度,但它绝不会导致   抛出的例外。

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

为了避免这个错误,你可能想先将十进制值标准化,有效地“删除”小数点后的额外0。这有点棘手,因为它涉及96位整数运算 - .NET 4.0 BigInteger类可能会使它更容易,但这可能不适合你。

答案 1 :(得分:25)

答案在于decimal试图保留有效位数。因此,8224055000.0000000000m有20位有效数字,存储为82240550000000000000E-10,而8224055000m只有10,存储为8224055000E+0double的尾数(逻辑上)为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,我使用点来分隔符号,指数和尾数字段;对于十进制,请参阅MSDN on decimal.GetBits,但基本上最后的96位是尾数。注意dcm2的尾数位和dbl2的最高有效位是如何完全一致的(不要忘记1尾数中隐含的double位,并且实际上这些位代表8224055000. dbl的尾数位与dcm2dbl2中的尾数位相同,但对于最低有效位中的讨厌1dcm的指数为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") ;

答案 2 :(得分:5)

文章What Every Computer Scientist Should Know About Floating-Point Arithmetic将是一个很好的起点。

简短的回答是浮点二进制算术必然是近似,并且它并不总是您猜测的近似值。这是因为CPU在基数2中进行算术,而人类(通常)在基数10中进行算术运算。由此产生了各种各样的意外效果。

答案 3 :(得分:2)

要更清楚地看到这个问题,可以在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

答案 4 :(得分:1)

这是一个老问题,并且一直是StackOverflow上许多类似问题的主题。

简单解释是十进制数不能用二进制

精确表示

This link是一篇可能解释问题的文章。