C# 具有自定义数字格式字符串的double.ToString(字符串)不会生成预期结果

C# 具有自定义数字格式字符串的double.ToString(字符串)不会生成预期结果,c#,double,C#,Double,我想显示具有特定小数位数的双值 double.ToString(字符串)自定义数字格式的文档说明: “0”自定义格式说明符用作零占位符符号。如果正在格式化的值在格式字符串中出现零的位置有一个数字,则该数字将复制到结果字符串中;否则,结果字符串中将显示零。小数点前最左边的零和小数点后最右边的零的位置决定了结果字符串中始终存在的数字范围 资料来源: double.ToString(string)文档中给出的所有示例都是我从上述引用中所期望的结果 然而,这似乎并不适用于我的所有情况: double d

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

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

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

资料来源:

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"

这里发生了什么?

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

大多数情况下,这很好,但当您达到精度极限时,情况开始出现故障,即双精度约为16位,
double
约为9位,
float

具体来说,双精度不能准确表示
63712373026.615219
。使用
G50
或,我们可以查看双精度表示的确切数字:

63712373026.615219.ToString("G50"); // 63712373026.6152191162109375
小数点后第7位可以,但看看与
63712373026.615219
最近的可表示数字实际上有多大

通过一些尝试和错误,我们可以看到值的范围,所有值都表示为
63712373026.61522191162109375

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.6152154
63712373026.6152229
之间的所有内容都存储为数字
63712373026.6152911162109375

这给格式化程序带来了一个问题:如果您要求
63712373026.615219.ToString(“0.000000”)
,它是否应该给您
63712373026.615223
63712373026.615215
或介于两者之间的任何内容

在实践中,它似乎要做的是计算出double可能代表的可能值的范围,然后四舍五入到所有人共有的数字。由于
63712373026.615229
63712373026.6152154
以及两者之间的所有内容都以
63712373026.6152
开头,这就是格式化程序的工作原理。这就是为什么如果您强制它,它将打印
63712373026.615200
:它知道它没有足够的信息来填充最后两位数字


请注意,我认为往返和
G17
格式有点误导您。往返基本上打印最少的数字,这些数字将被解析回相同的基础双精度值。因此
63712373026.615219
包含解析回
63712373026.61522191162109375
的最小小数位数

请注意,它们修复了.NET 5上的
R

63712373026.615219.ToString("R"); // 63712373026.61522
G17
仅打印17位数字,而不考虑双精度的基本值。因为double只有16位左右的精度,这也足以安全地往返double

这可以通过更简单的值看到,例如
0.1
double
,不是基数10,不能准确表示
0.1
。相反,其最接近的值为:

0.1.ToString("G99"); // 0.1000000000000000055511151231257827021181583404541015625
然而:

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

表示为
0.100000000000000551151231257827021181583404540105625
的最短值是
0.1
,因此这就是
R
返回的值,即使它与底层表示不完全匹配。这很好,因为解析
0.1
将产生一个double,它的底层表示形式是
0.10000000000000055115123125782702118158340451015625
,从而成功地将其往返。

注意,这里有18位精度,大约在双精度的极限。期待四舍五入的怪异。是的,这是我最初的想法,但往返fromat说明符证明此值可以完全打印,因此我希望自定义格式说明符也可以这样做。这是一个伟大而全面的答案,但我仍然认为,根据文档,根据我的预期,它应该是63712373026.615219,不是因为这是我输入的数字,而是因为它是63712373026.6152191162109375的适当截断版本。因此,63712373026.615223.ToString(“0.000000”)也应为63712373026.615219。所以我想说你的答案是正确的,但是微软的文档显然是错误的。您对该格式说明符的猜测似乎比文档更准确。我怀疑他们将其作为实现细节,文档仅涵盖double具有足够精度来表示所讨论的值的情况。我相信运行时使用了Grisu3/Dragon4算法:也许它们提供了更多的细节?我的猜测是
“0”
被定义为对浮点数的Dragon4十进制表示进行操作,而不是对其原始基础值进行操作。这是有道理的,但我没有更多的直觉来支持它