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

Posted

技术标签:

【中文标题】在 C# 中将十进制数转换为双精度数会产生差异【英文标题】:Conversion of a decimal to double number in C# results in a difference 【发布时间】:2010-12-07 17:53:29 【问题描述】:

问题总结:

对于某些十进​​制值,当我们将类型从十进制转换为双精度时,会在结果中添加一小部分。

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

代码示例:

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

查看 cmets 中的结果。结果是从调试器的手表中复制的。 产生这种效果的数字的小数位数远远少于数据类型的限制,所以它不可能是溢出(我猜!)。

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

我想这应该与两种数据类型中数字的按位表示的差异有关,但不知道是什么!而且我需要知道该怎么做才能按照我需要的方式进行转换。 (如 dcm2 -> dbl2)

【问题讨论】:

我已经在 MS Connect 上报告了这个问题。这是链接:connect.microsoft.com/VisualStudio/feedback/… 我不确定是什么原因,但似乎问题出在 (6) 个大小数位上。我测试了 5 个小数位并且工作正常。我有类似的情况,我从十进制转换为双精度并返回,因为我的精度只有 2 位小数,所以我的代码是安全转换的。 【参考方案1】:

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

这里有一个稍微简单的演示,使用我之前使用过几次的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 - 最终会包含额外的数据。我强烈怀疑这是由于用于从 @ 转换的算法中的怪癖987654326@ 到 double,但很不幸。

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

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

“最近的双精度值”显然只是 8224055000...所以这是 IMO 的错误。不过,这不是我希望很快得到解决的问题。 (顺便说一下,它在 .NET 4.0b1 中给出了相同的结果。)

为避免该错误,您可能需要先规范化十进制值,有效地“删除”小数点后多余的 0。这有点棘手,因为它涉及 96 位整数运算 - .NET 4.0 BigInteger 类可能会使其更容易,但这可能不是您的选择。

【讨论】:

这也是 IMO 的一个错误。您/任何人是否已将此情况报告给 Microsoft?我正在搜索 MS Connect,但看不到任何相关内容。所以,我发布它。只是想知道他们是否确认这是一个错误。 在这种特殊情况下不需要 96 位算术,因为可以让 decimal 完成繁重的工作:) 迷人的错误!正如 Anton Tykhyy 所指出的,这几乎可以肯定是因为具有很多额外精度的小数表示不再是“本机”在适合双精度且没有表示错误的整数中。我愿意赌上一美元,这个错误在 OLE 自动化中已经存在了 15 年——我们使用 OA 库进行十进制编码。我的机器上碰巧有一个十年前的 OA 资源档案;如果我明天有空我会去看看。 客户支持没有比这更好的了 :) @Jon,在 MS Connect(C# 规范部分)上报告此问题时,我使用了您的部分答案。感谢您的信息。【参考方案2】:

答案在于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

对于双精度,我使用点来分隔符号、指数和尾数字段;对于十进制,请参阅MSDN on decimal.GetBits,但本质上最后 96 位是尾数。注意dcm2 的尾数位和dbl2 的最高有效位完全一致(不要忘记double 的尾数中隐含的1 位),实际上这些位表示8224055000。 dbl 的尾数位与 dcm2dbl2 中的相同,但在最低有效位中是讨厌的 1dcm的指数为10,尾数为82240550000000000000。

更新二:实际上很容易去掉尾随零。

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

【讨论】:

这是有道理的,但请参阅 Jon Skeet 的回答。从逻辑上讲,指定更多有效数字应该会导致更准确的转换,而不是更糟糕的转换!有没有办法将小数转换为具有“较少”有效数字的小数?在我的情况下,这应该会带来更好的转换! 转换更准确——你得到6个额外的数字——但结果不是你所期望的,因为十进制和双精度的基数不同。我稍后会添加示例。 这不是更准确的转换。小数点的确切值是可用的,所以应该返回。我知道为什么会这样,但这并不正确:) 好吧,如果你从这个意义上理解“准确”,我同意。 至于“准确” - 一个相当简单的准确度衡量标准是“开始表示的确切数字与转换结果的确切值之间有什么区别”? 0 表示完全准确 - 至少就数字的大小而言,并且在这种情况下可用。我正是这个意思。由于 double 没有“有效位数”的概念,我不相信可以用这些术语来衡量准确性。 (它可以用于其他转换,例如转换为 确实 保留有效位数的另一种类型。)【参考方案3】:

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

简短的回答是浮点二进制算术必然是一个近似值,它并不总是你会猜到的近似值。这是因为 CPU 以 2 为基数进行算术运算,而人类(通常)以 10 为基数进行算术运算。由此产生的各种意想不到的影响。

【讨论】:

感谢文章链接,虽然很长,但我会努力阅读。 Base 2 算术与 Base 10 算术是我所怀疑的,但有两点:1. decimal 有 28-29 个有效数字,double 有 15-16 个有效数字。我的号码有 8 位有效数字就足够了。为什么要这样对待?而且只要有原始数字的双精度表示,为什么转换会导致另一个数字? 2. 两个“相同”的十进制值被转换成不同的双精度值会怎样? 有效数字的数量并不特别相关 - “0.1”只有一个有效数字,但仍然无法以浮点/双精度表示。关于那里存在一个可用的精确表示的观点更为重要。至于给出不同双打的两个值 - 它们相等但它们不相同 有没有办法将这些“相等但不相同”的小数相互转换?有没有办法在调试器中看到它? (我想我应该看到按位表示,但是 VS 中没有这样的选项。而且“十六进制显示”也不能这样工作) Decimal.GetBits 将为您提供按位表示 - 您希望通过这种方式进行标准化。这并不容易:(你知道这个值是实际上一个整数吗?如果是这样,那会有所帮助...... 对于这个实例,这个数字“实际上”是一个整数。但它可以是非整数。可以肯定的是,它没有(也不会)有 16 位有效数字。【参考方案4】:

要更清楚地说明此问题,请在 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

【讨论】:

我认为了解发生了什么的关键是打印出 2E23/1E21 和 2E25/2E23。 Decimaldouble 的转换是通过将整数值除以 10 的幂来执行的,即使这会引入舍入误差。【参考方案5】:

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

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

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

【讨论】:

其实这并不能解释。 许多十进制数不能完全用二进制表示 - 但在这种情况下,输入 可以 用二进制准确表示。数据正在不必要地丢失。 Jon,数据并没有丢失,相反,问题在于不必要地保存(来自 Irchi 的 POV,无意冒犯)数据。 Anton,请参阅 Jon 发布的规范。不必要地保留的数据不应破坏转换。在 16 位有效数字之后,十进制值指定所有数字为“0”。为什么要在第16位四舍五入为“1”?! “0”比“1”更接近“精确”十进制值。 我不知道“应该”,不是一个标准的人——但这就是它的行为方式,唯一的问题是如何处理这种行为。 @Jon,我在我的回答中强调“简单化”这个词,记录在案。

以上是关于在 C# 中将十进制数转换为双精度数会产生差异的主要内容,如果未能解决你的问题,请参考以下文章

在c#中将十进制值转换为两个精度[重复]

在 C# 中将字符串转换为双精度

在java中将十六进制数字字符串转换为双精度数字

如何在R中将两个变量(整数)转换为双精度?

Ruby 将 64 位 IEEE 754 十六进制转换为双精度

C#类型转换