.NET 中的 Math.Round 是否错误地舍入了某些值?
Does Math.Round in .NET round certain values incorrectly?
例如,
double a = 2.55;
double b = Math.Round(a, 1); // expected result is 2.5
Console.WriteLine(b); // 2.6
我们期望 2.5
的原因是最接近 2.55
的 64 位 IEEE 754 浮点数正好是 2.54999999999999982236431605997495353221893310546875
,所以我们是否使用 MidpointRounding.ToEven
或 MidpointRounding.AwayFromZero
该值应向下舍入为 2.5
。
另一方面,“F”格式说明符似乎可以正确处理舍入。
double a = 2.55;
Console.WriteLine($"{a:F1}"); // 2.5
编辑: 看起来 .NET 团队正在跟踪与 Math.Round
here. According to this 基本相同的问题,该问题可能会在即将到来的.NET 7 但不确定。
.NET 中舍入的默认实现是“四舍五入”,即“银行四舍五入”。这意味着 mid-point 值四舍五入到最接近的偶数。
您实际问题的答案“.NET 中的 Math.Round 是否错误地舍入了某些值?”是:是。 (好吧,Microsoft 可能会争辩说此行为是已定义的,因此是正确的。)
原因在the documentation for Math.Round()
中有描述:
Because of the loss of precision that can result from representing
decimal values as floating-point numbers or performing arithmetic
operations on floating-point values, in some cases the Round(Double,
Int32, MidpointRounding) method may not appear to round midpoint
values as specified by the mode parameter. This is illustrated in the
following example, where 2.135 is rounded to 2.13 instead of 2.14.
This occurs because internally the method multiplies value by
10^digits, and the multiplication operation in this case suffers from a
loss of precision.
我们可以测试这个:
double a = 2.55;
double c = a * Math.Pow(10, 1); // "a * 10.0" gives the same result.
Console.WriteLine(a.ToString("f16"));
Console.WriteLine(c.ToString("f16"));
输出为:
2.5499999999999998
25.5000000000000000
可以看到乘以10^1后的值为25.5,将在下一步的舍入算法中舍入。
你可以看看the actual implementation here.
有点繁琐,但答案确实是“某事某事某事”;)
Math.Round(value,decimal)
按预期工作。您将您的值定义为 2.55
并认为您在 IEEE 754 中将 2.55
又名 2.54999999...
四舍五入,但这是错误的。带小数的四舍五入对四舍五入应用 10 的幂。所以你的 2.55
有 1 个小数点是应用 10 的单个幂所以它变成 25.5
这在 IEEE 754 中完美地表示为 25.5
。然后四舍五入变成 26.0
然后它除以系数回到 2.6
因此你的结果。
因为2.55是中间值,所以四舍五入的时候按照惯例。默认这是“To Even”。所以在这种情况下,它四舍五入到最接近的偶数 2.6。
如果您使用 AwayFromZero,在这种情况下它会做同样的事情,如果您四舍五入 2.65,它会给出不同的结果。
奇怪,我用字符串格式化程序得到了相同的结果...?也许这是一种文化环境。另见 Convert Double to String: Culture specific
例如,
double a = 2.55;
double b = Math.Round(a, 1); // expected result is 2.5
Console.WriteLine(b); // 2.6
我们期望 2.5
的原因是最接近 2.55
的 64 位 IEEE 754 浮点数正好是 2.54999999999999982236431605997495353221893310546875
,所以我们是否使用 MidpointRounding.ToEven
或 MidpointRounding.AwayFromZero
该值应向下舍入为 2.5
。
另一方面,“F”格式说明符似乎可以正确处理舍入。
double a = 2.55;
Console.WriteLine($"{a:F1}"); // 2.5
编辑: 看起来 .NET 团队正在跟踪与 Math.Round
here. According to this 基本相同的问题,该问题可能会在即将到来的.NET 7 但不确定。
.NET 中舍入的默认实现是“四舍五入”,即“银行四舍五入”。这意味着 mid-point 值四舍五入到最接近的偶数。
您实际问题的答案“.NET 中的 Math.Round 是否错误地舍入了某些值?”是:是。 (好吧,Microsoft 可能会争辩说此行为是已定义的,因此是正确的。)
原因在the documentation for Math.Round()
中有描述:
Because of the loss of precision that can result from representing decimal values as floating-point numbers or performing arithmetic operations on floating-point values, in some cases the Round(Double, Int32, MidpointRounding) method may not appear to round midpoint values as specified by the mode parameter. This is illustrated in the following example, where 2.135 is rounded to 2.13 instead of 2.14. This occurs because internally the method multiplies value by 10^digits, and the multiplication operation in this case suffers from a loss of precision.
我们可以测试这个:
double a = 2.55;
double c = a * Math.Pow(10, 1); // "a * 10.0" gives the same result.
Console.WriteLine(a.ToString("f16"));
Console.WriteLine(c.ToString("f16"));
输出为:
2.5499999999999998
25.5000000000000000
可以看到乘以10^1后的值为25.5,将在下一步的舍入算法中舍入。
你可以看看the actual implementation here.
有点繁琐,但答案确实是“某事某事某事”;)
Math.Round(value,decimal)
按预期工作。您将您的值定义为 2.55
并认为您在 IEEE 754 中将 2.55
又名 2.54999999...
四舍五入,但这是错误的。带小数的四舍五入对四舍五入应用 10 的幂。所以你的 2.55
有 1 个小数点是应用 10 的单个幂所以它变成 25.5
这在 IEEE 754 中完美地表示为 25.5
。然后四舍五入变成 26.0
然后它除以系数回到 2.6
因此你的结果。
因为2.55是中间值,所以四舍五入的时候按照惯例。默认这是“To Even”。所以在这种情况下,它四舍五入到最接近的偶数 2.6。 如果您使用 AwayFromZero,在这种情况下它会做同样的事情,如果您四舍五入 2.65,它会给出不同的结果。
奇怪,我用字符串格式化程序得到了相同的结果...?也许这是一种文化环境。另见 Convert Double to String: Culture specific