格式化C#中输出的双精度数
-
07-07-2019 - |
题
运行与相关的双重乘法功能的快速实验?并阅读了几篇关于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));
}
这个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 );
}
然而,C#产生输出:
6.9
6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000
尽管我在调试器中显示的值等于6.89999999999999946709(而不是6.9)。
与C比较,显示格式要求的精度:
6.900000
6.89999999999999946709
+ 0.00000000000000088818
= 6.90000000000000035527
发生了什么事?
(Microsoft .NET Framework版本3.51 SP1 / Visual Studio C#2008 Express Edition)
我有数值计算的背景和经验实现区间算术 - 一种在各种平台上估算由于复杂数值系统的精度限制而导致的误差的技术。要获得赏金,请不要尝试解释存储精度 - 在这种情况下,它是64位双精度的一个ULP的差异。
为了得到赏金,我想知道.Net是如何(或是否)将双倍格式化为C代码中可见的请求精度。
解决方案
问题是,在应用格式之前,.NET将始终将 double
舍入到15个有效小数位,无论格式请求的精度如何,无论二进制数的精确十进制值。
我猜测Visual Studio调试器有自己的格式/显示例程,可以直接访问内部二进制数,因此C#代码,C代码和调试器之间存在差异。
没有任何内置功能可以让您访问 double
的确切十进制值,或者使您能够将 double
格式化为特定的十进制数地方,但您可以通过挑选内部二进制数并将其重建为十进制值的字符串表示来自己完成此操作。
或者,你可以使用Jon Skeet的 DoubleConverter
类(链接)来自他的“Binary floating point and .NET”文章)。它有一个 ToExactString
方法,它返回 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
其他提示
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);
请参阅此 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);
输出
6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000
虽然这个问题同时关闭,但我相信值得一提的是这种暴行是如何形成的。在某种程度上,你可能会责怪C#规范,它规定双精度必须具有15或16位数的精度(IEEE-754的结果)。更进一步(第4.1.6节),它声明允许实现使用更高精度。请注意:更高,而不是更低。它们甚至被允许偏离IEEE-754: x * y / z
类型的表达式,其中 x * y
将产生 +/- INF
但是在划分后会处于有效范围内,不必导致错误。此功能使编译器更容易在体系结构中使用更高的精度,从而产生更好的性能。
但我答应了“理由”。以下是来自共享源CLI ,在 clr / src / vm / comnumber.cpp
中:
"为了给出两者都有的数字 友好的展示和 圆形的,我们解析数字 使用15位数,然后确定是否 它往返相同的价值。如果 它确实如此,我们将该NUMBER转换为a 字符串,否则我们使用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浮点,请考虑这个在线工具,它显示了浮点的所有相关部分。
使用
Console.WriteLine(String.Format(" {0:G17}", i));
这将为您提供所有17位数字。默认情况下,Double值包含15个十进制数字的精度,但内部最多保留17位数。 {0:R}不会总是给你17位数字,如果数字可以用那个精度表示,它将给出15位数。
如果数字可以用该精度表示,则返回15位数;如果数字只能用最大精度表示,则返回17位数。没有任何事情可以使双重返回更多的数字,这是它的实现方式。如果你不喜欢它自己做一个新的双课......
.NET的双重存储不能存储超过17的数字,所以你不能在调试器中看到6.89999999999999946709,你会看到6.8999999999999995。请提供一张图片来证明我们的错误。
答案很简单,可以在 MSDN <上找到。 / A>
请记住,浮点数只能近似一个十进制数,并且浮点数的精度决定了该数字接近十进制数的准确程度。默认情况下,Double值包含 15位精度的十进制数字,但内部最多保留17位数。
在您的示例中,i的值为6.89999999999999946709,对于第3位和第16位之间的所有位置,其值为9(记住计算数字中的整数部分)。转换为字符串时,框架将数字四舍五入到第15位。
i = 6.89999999999999 946709
digit = 111111 111122
1 23456789012345 678901
我试图重现你的发现,但当我在调试器中看到'i'时,它显示为'6.8999999999999995'而不是你在问题中写的'6.89999999999999946709'。你能提供重现所见内容的步骤吗?
要查看调试器显示的内容,您可以使用DoubleConverter,如以下代码行所示:
Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));
希望这有帮助!
编辑:我想我比我想的更累,当然这与格式化到往返值相同(如前所述)。
答案是肯定的,在.NET中打破双重打印,它们正在打印尾随垃圾数字。
您可以阅读如何正确实现它此处
我必须为IronScheme做同样的事情。
> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995
注意:C和C#都有正确的值,只是打破了打印。
更新:我仍然在寻找导致这一发现的邮件列表对话。
我发现了这个快速修复。
double i = 10 * 0.69;
System.Diagnostics.Debug.WriteLine(i);
String s = String.Format("{0:F20}", i).Substring(0,20);
System.Diagnostics.Debug.WriteLine(s + " " +s.Length );
Console.WriteLine(string.Format(&quot; Course Fees is {0:0.00}&quot;,+ cfees));
internal void DisplaycourseDetails()
{
Console.WriteLine("Course Code : " + cid);
Console.WriteLine("Couse Name : " + cname);
//Console.WriteLine("Couse Name : " + string.Format("{0:0.00}"+ cfees));
// string s = string.Format("Course Fees is {0:0.00}", +cfees);
// Console.WriteLine(s);
Console.WriteLine( string.Format("Course Fees is {0:0.00}", +cfees));
}
static void Main(string[] args)
{
Course obj1 = new Course(101, "C# .net", 1100.00);
obj1.DisplaycourseDetails();
Course obj2 = new Course(102, "Angular", 7000.00);
obj2.DisplaycourseDetails();
Course obj3 = new Course(103, "MVC", 1100.00);
obj3.DisplaycourseDetails();
Console.ReadLine();
}
}