格式化C#中输出的双精度数

Pet*_*ham 62 c# floating-point formatting ieee-754

在.NET中运行与双倍乘法相关的快速实验并阅读了几篇关于C#字符串格式的文章,我认为这样:

{
    double i = 10 * 0.69;
    Console.WriteLine(i);
    Console.WriteLine(String.Format("  {0:F20}", i));
    Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
    Console.WriteLine(String.Format("= {0:F20}", 6.9));
}
Run Code Online (Sandbox Code Playgroud)

将是这个C代码的C#等价物:

{
    double i = 10 * 0.69;

    printf ( "%f\n", i );
    printf ( "  %.20f\n", i );
    printf ( "+ %.20f\n", 6.9 - i );
    printf ( "= %.20f\n", 6.9 );
}
Run Code Online (Sandbox Code Playgroud)

但是C#会产生输出:

6.9
  6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000
Run Code Online (Sandbox Code Playgroud)

尽管我在调试器中显示的值等于6.89999999999999946709(而不是6.9).

与C比较,显示格式要求的精度:

6.900000                          
  6.89999999999999946709          
+ 0.00000000000000088818          
= 6.90000000000000035527          
Run Code Online (Sandbox Code Playgroud)

这是怎么回事?

(Microsoft .NET Framework版本3.51 SP1/Visual Studio C#2008 Express Edition)


我有数值计算的背景和经验实现区间算法 - 一种在各种平台上估算复杂数值系统中由于精度极限而导致的误差的技术.要获得赏金,请不要尝试解释存储精度 - 在这种情况下,它是64位双精度的一个ULP的差异.

为了得到赏金,我想知道.Net是如何(或是否)将双精度格式化为C代码中可见的请求精度.

Luk*_*keH 62

问题是,应用格式化之前,.NET将始终将a double到15个有效十进制数字舍入,无论格式请求的精度如何,也不管二进制数的精确十进制值如何.

我猜Visual Studio调试器有自己的格式/显示例程,直接访问内部二进制数,因此C#代码,C代码和调试器之间存在差异.

没有任何内置允许您访问a的精确十进制值double,或者允许您将a格式化double为特定数量的小数位,但您可以通过挑选内部二进制数并将其重建为十进制值的字符串表示形式.

或者,您可以使用Jon Skeet的DoubleConverter(链接到他的"二进制浮点和.NET"文章).这有一个ToExactString方法,它返回a的精确十进制值double.您可以轻松修改此选项以启用输出舍入到特定精度.

double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));

// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625
Run Code Online (Sandbox Code Playgroud)

  • 不,这些不是垃圾数字,这些是十进制数的二进制表示的最接近近似的十进制表示.这不是任意的,只是基于IEEE-754规范的纯数学和52位分数.你提到的数字是四舍五入的数字.例如,中间数字等于"1/2 ^ 50",换句话说,它是一位关闭. (14认同)
  • 它们是垃圾,因为它们可以在不丢失任何值的情况下被抛弃 - 双值表示实数行上的间隔,而不是精确值,因此6.8999999999999995与'确切'一样正确值版本,因为两者都落在间隔.有三种不同的行为在某种程度上都是正确的 - 具有最小位数的十进制值在区间内(往返值或leppie的方案值),区间的中心(上面的'精确'值)或精确值两侧的ULP半个范围. (5认同)
  • 你有一个参考".NET将在应用你的格式之前总是将一个双精度数加到15位有效十进制数字"吗? (2认同)
  • @PeteKirkham 这通常是正确的,但有时了解双精度值的确切值很重要。我从事模拟工作,并且经常将双输入的值记录到方法中。我希望如果我在以后的模拟运行中将相同的双精度值传递给相同的方法,该方法将给出完全相同的结果。这是一个著名的例子,说明微小的舍入误差影响模拟:https://royalsocietypublishing.org/doi/pdf/10.1098/rsbm.2009.0004(第143页,最后两段)。 (2认同)

小智 22

Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567);      // "123.46"
String.Format("{0:0.00}", 123.4);         // "123.40"
String.Format("{0:0.00}", 123.0);         // "123.00"

// max. two decimal places
String.Format("{0:0.##}", 123.4567);      // "123.46"
String.Format("{0:0.##}", 123.4);         // "123.4"
String.Format("{0:0.##}", 123.0);         // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567);      // "123.5"
String.Format("{0:00.0}", 23.4567);       // "23.5"
String.Format("{0:00.0}", 3.4567);        // "03.5"
String.Format("{0:00.0}", -3.4567);       // "-03.5"

Thousands separator
String.Format("{0:0,0.0}", 12345.67);     // "12,345.7"
String.Format("{0:0,0}", 12345.67);       // "12,346"

Zero
Following code shows how can be formatted a zero (of double type).

String.Format("{0:0.0}", 0.0);            // "0.0"
String.Format("{0:0.#}", 0.0);            // "0"
String.Format("{0:#.0}", 0.0);            // ".0"
String.Format("{0:#.#}", 0.0);            // ""

Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567);    // "     123.5"
String.Format("{0,-10:0.0}", 123.4567);   // "123.5     "
String.Format("{0,10:0.0}", -123.4567);   // "    -123.5"
String.Format("{0,-10:0.0}", -123.4567);  // "-123.5    "

Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567);   // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567);  // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0);        // "zero"

Some funny examples
String.Format("{0:my number is 0.0}", 12.3);   // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);
Run Code Online (Sandbox Code Playgroud)

  • 我自己可以找到http://www.csharp-examples.net/string-format-double/; 问题是当请求的位数值与变量的精度不一致时的行为. (4认同)

Tim*_*ers 8

看看这个MSDN参考.它在说明中指出数字四舍五入到所请求的小数位数.

如果您使用"{0:R}",它将产生所谓的"往返"值,请查看此MSDN参考以获取更多信息,这是我的代码和输出:

double d = 10 * 0.69;
Console.WriteLine("  {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);
Run Code Online (Sandbox Code Playgroud)

产量

  6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000
Run Code Online (Sandbox Code Playgroud)


Abe*_*bel 8

虽然这个问题同时关闭,但我相信值得一提的是这种暴行是如何产生的.在某种程度上,你可能会责怪C#规范,它规定双精度必须具有15或16位数的精度(IEEE-754的结果).再进一步说明(第4.1.6节),它表明允许实现使用更高的精度.请注意:更高,而不是更低.他们甚至允许从IEEE-754偏离:类型的表达式x * y / z,其中x * y将产生+/-INF,但将是一个有效的范围分割后,不必导致错误.此功能使编译器可以更轻松地在体系结构中使用更高的精度,从而获得更好的性能.

但我答应了"理由".这里有一个引号(您请求在你最近的评论一个资源)从共享源代码CLIclr/src/vm/comnumber.cpp:

"为了给出显示友好且可循环播放的数字,我们使用15位数解析数字,然后确定它是否往返到相同的值.如果是,我们将该NUMBER转换为字符串,否则我们使用17位数进行重新分析并显示该数字."

换句话说:MS的CLI开发团队决定既可以进行循环播放,也可以显示非常难以理解的值.是好是坏?我希望选择加入或选择退出.

它的作用是找出任何给定数字的这种可循环性?转换为通用NUMBER结构(具有双重属性的单独字段)和返回,然后比较结果是否不同.如果它不同,则使用精确值(如中间值所示6.9 - i)如果相同,则使用"漂亮值".

正如您在对Andyp的评论中已经提到的那样,它6.90...00是等于的6.89...9467.现在你知道为什么0.0...8818被使用了:它有点不同0.0.

这个15位数的屏障是硬编码的,只能通过重新编译CLI,使用Mono或通过调用Microsoft并说服他们添加一个选项来打印完整的"精度"来改变它(这不是真正的精确,但由于缺乏一个更好的词).自己计算52位精度或使用前面提到的库可能更容易.

编辑:如果您想尝试使用IEE-754浮点,请考虑使用此在线工具,该工具向您显示浮点的所有相关部分.


小智 5

使用

Console.WriteLine(String.Format("  {0:G17}", i));
Run Code Online (Sandbox Code Playgroud)

这将为您提供所有的17位数字。默认情况下,Double值包含15个精度的十进制数字,尽管内部最多保留17个数字。{0:R}不会总是为您提供17位数字,如果可以用这种精度表示数字,它将给出15位数字。

如果可以用精度表示数字,则返回15位数字;如果只能以最大精度表示数字,则返回17位数字。您无法做任何事情来使double返回更多位数,这是实现它的方式。如果您不喜欢,自己动手做一个新的双班...

.NET的double不能存储多于17的数字,因此在调试器中看不到6.89999999999999946709,而在6.8999999999999995中看不到。请提供图片以证明我们错了。