在数学上,考虑这个问题的有理数
8725724278030350 / 2**48
其中**
,分母表示取幂,即分母为2
th48
次方。(分数不是最低的,可以减少 2。)这个数字完全可以表示为System.Double
. 它的十进制展开是
31.0000000000000'49'73799150320701301097869873046875 (exact)
其中撇号不代表缺失的数字,而只是标记四舍五入到15的边界。将执行17位数字。
请注意以下几点:如果此数字四舍五入为 15 位,则结果将为31
(后跟 130
个 s),因为接下来的数字 ( 49...
) 以 a 开头(表示向下4
舍入)。但如果先将数字四舍五入为 17 位,然后再四舍五入为 15 位,则结果可能为. 这是因为第一个舍入通过将数字增加到(下一个数字是)来进行舍入,然后第二个舍入可能会再次舍入(当中点舍入规则说“从零舍入”时)。31.0000000000001
49...
50 (terminates)
73...
(当然,具有上述特征的数字还有很多。)
现在,事实证明这个数字的 .NET 标准字符串表示是"31.0000000000001"
. 问题:这不是一个错误吗?通过标准字符串表示,我们指的String
是由 parameterlesDouble.ToString()
实例方法生成的,它当然与由ToString("G")
.
需要注意的一件有趣的事情是,如果您将上述数字转换为,System.Decimal
那么您将得到一个decimal
完全正确的数字31
!请参阅此 Stack Overflow 问题Double
,了解关于将 a 转换为Decimal
涉及第一次四舍五入到 15 位这一令人惊讶的事实的讨论。这意味着强制转换Decimal
为 15 位数字是正确的,而调用ToSting()
是错误的。
总而言之,我们有一个浮点数,当输出给用户时,它是31.0000000000001
,但当转换为Decimal
(29位可用)时,它变成31
了精确的。这是不幸的。
这里有一些 C# 代码供您验证问题:
static void Main()
{
const double evil = 31.0000000000000497;
string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx
Console.WriteLine("Exact value (Jon Skeet): {0}", exactString); // writes 31.00000000000004973799150320701301097869873046875
Console.WriteLine("General format (G): {0}", evil); // writes 31.0000000000001
Console.WriteLine("Round-trip format (R): {0:R}", evil); // writes 31.00000000000005
Console.WriteLine();
Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));
Console.WriteLine();
decimal converted = (decimal)evil;
Console.WriteLine("Decimal version: {0}", converted); // writes 31
decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
Console.WriteLine("Better decimal: {0}", preciseDecimal); // writes 31.000000000000049737991503207
}
上面的代码使用了 Skeet 的ToExactString
方法。如果你不想使用他的东西(可以通过网址找到),只需删除上面依赖的代码行exactString
。您仍然可以看到有Double
问题的 ( evil
) 是如何四舍五入和投射的。
添加:
好的,所以我测试了更多的数字,这里有一个表格:
exact value (truncated) "R" format "G" format decimal cast
------------------------- ------------------ ---------------- ------------
6.00000000000000'53'29... 6.0000000000000053 6.00000000000001 6
9.00000000000000'53'29... 9.0000000000000053 9.00000000000001 9
30.0000000000000'49'73... 30.00000000000005 30.0000000000001 30
50.0000000000000'49'73... 50.00000000000005 50.0000000000001 50
200.000000000000'51'15... 200.00000000000051 200.000000000001 200
500.000000000000'51'15... 500.00000000000051 500.000000000001 500
1020.00000000000'50'02... 1020.000000000005 1020.00000000001 1020
2000.00000000000'50'02... 2000.000000000005 2000.00000000001 2000
3000.00000000000'50'02... 3000.000000000005 3000.00000000001 3000
9000.00000000000'54'56... 9000.0000000000055 9000.00000000001 9000
20000.0000000000'50'93... 20000.000000000051 20000.0000000001 20000
50000.0000000000'50'93... 50000.000000000051 50000.0000000001 50000
500000.000000000'52'38... 500000.00000000052 500000.000000001 500000
1020000.00000000'50'05... 1020000.000000005 1020000.00000001 1020000
第一列给出了Double
代表的确切(尽管被截断)值。第二列给出了来自"R"
格式字符串的字符串表示。第三列给出了通常的字符串表示。最后第四列给出了System.Decimal
转换 this 的结果Double
。
我们得出以下结论:
- 在很多情况下
ToString()
,通过转换舍入到 15 位和舍入到 15 位不同意Decimal
- 在很多情况下转换为
Decimal
也舍入不正确,并且这些情况下的错误不能描述为“round-twice”错误 - 在我的情况下,当他们不同意时,
ToString()
似乎产生比Decimal
转换更大的数字(无论两轮中的哪一个正确)
我只尝试了上述情况。我还没有检查其他“表格”的数量是否存在舍入错误。