double.ToString(string) 与自定义数字格式字符串不产生预期的结果

double.ToString(string) with custom numeric format string does not produce the expected result

我想显示具有特定小数位数的双精度值。

double.ToString(string) 的自定义数字格式文档说:

“0”自定义格式说明符用作零占位符。如果正在格式化的值在格式字符串中出现零的位置有一个数字,则将该数字复制到结果字符串;否则,结果字符串中会出现一个零。小数点前最左边零和小数点后最右边零的位置决定了结果字符串中始终存在的数字范围。

来源:https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings#Specifier0

double.ToString(string) 文档中给出的所有示例都符合我对上述引述的期望。

然而,这似乎并不适用于所有情况:

double d = 63712373026.615219;
d.ToString("R"); // "63712373026.615219"
d.ToString("G17"); // "63712373026.615219"
d.ToString("0.000000") // "63712373026.615200"
d.ToString("#.######") // "63712373026.6152"

这是怎么回事?

浮点数是近似值:它们的位数有限,并且会在可用位数的限制范围内尽力表示接近您要求的数字。

大多数时候这工作正常,但当你达到精度极限时,事情开始崩溃,double 大约 16 位数字,float 大约 9 位数字。

具体来说,double 不能准确表示 63712373026.615219。使用G50Jon Skeet's DoubleConverter,我们可以看一下double所代表的确切数字:

63712373026.615219.ToString("G50"); // 63712373026.6152191162109375

我们可以精确到小数点后第 7 位,但是看看最接近 63712373026.615219 的可表示数字实际上是如何大一点?

通过反复试验,我们可以看到值的范围都表示为 63712373026.6152191162109375:

63712373026.6152230.ToString("G50"); // 63712373026.61522674560546875
63712373026.6152229.ToString("G50"); // 63712373026.6152191162109375
63712373026.615219.ToString("G50");  // 63712373026.6152191162109375
63712373026.6152154.ToString("G50"); // 63712373026.6152191162109375
63712373026.6152153.ToString("G50"); // 63712373026.61521148681640625

double 的精度限制意味着 63712373026.615215463712373026.6152229 之间的所有内容都存储为数字 63712373026.6152191162109375

这给格式化程序带来了一个问题:如果您要求 63712373026.615219.ToString("0.000000"),它应该给您 63712373026.61522363712373026.615215 还是介于两者之间的任何值?

实际上,它所做的似乎是计算出 double 可能表示的可能值的范围,然后四舍五入到所有人共有的数字。由于 63712373026.615222963712373026.6152154 以及介于两者之间的所有内容都以 63712373026.6152 开头,这就是格式化程序所使用的。这就是为什么如果你强制它会打印 63712373026.615200 的原因:它知道它没有足够的信息来填写最后 2 位数字。


请注意,我认为往返格式和 G17 格式有点误导您。往返基本上打印 fewest 位,这些位将被解析回相同的底层双精度值。所以 63712373026.615219 包含最小的小数位数,它被解析回 63712373026.6152191162109375.

请注意,他们在 .NET 5 上修复了 R

63712373026.615219.ToString("R"); // 63712373026.61522

G17 只打印 17 位数字,而不管 double 的基础值如何。因为 double 只有大约 16 位精度,这也足以安全地 round-trip the double.

这可以用更简单的值看出,例如0.1double,不是以 10 为底,不能准确表示 0.1。相反,它最接近的值是:

0.1.ToString("G99"); // 0.1000000000000000055511151231257827021181583404541015625

但是:

0.1.ToString("R"); // 0.1

表示为0.1000000000000000055511151231257827021181583404541015625最短值是0.1,所以这就是Rreturns,甚至尽管它 完全 不匹配底层表示。这很好,因为解析 0.1 将产生一个 double,它的底层表示是 0.1000000000000000055511151231257827021181583404541015625,从而成功地将它往返。