在.NET中,当最后一位数字为5时的四舍五入规则是什么?

45

这是我的代码:

using static System.Console;

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            double[] doubles = new[] { 9.05, 9.15, 9.25, 9.35, 9.45, 9.55, 9.65, 9.75, 9.85, 9.95 };
            foreach (double n in doubles)
            {
                WriteLine("{0} ===> {1:F1}", n, n);
            }

        }
    }
}

.NET Framework 4.7.2的输出:

9.05 ===> 9.1
9.15 ===> 9.2
9.25 ===> 9.3
9.35 ===> 9.4
9.45 ===> 9.5
9.55 ===> 9.6
9.65 ===> 9.7
9.75 ===> 9.8
9.85 ===> 9.9
9.95 ===> 10.0

.NET 6 中的输出(使用相同的代码):

9.05 ===> 9.1
9.15 ===> 9.2
9.25 ===> 9.2
9.35 ===> 9.3
9.45 ===> 9.4
9.55 ===> 9.6
9.65 ===> 9.7
9.75 ===> 9.8
9.85 ===> 9.8
9.95 ===> 9.9

因此,在 .NET Framework 中,数字的四舍五入方式就像我们在学校里学到的那样。这被称为维基百科中的“round half up”。

但是在 .NET 6 中,9.05、9.15、9.55、9.65、9.75会向上舍入,而9.25、9.35、9.45、9.85、9.95会向下舍入。

我知道有一个规则叫做“round half to even” - 四舍五入到最近的值;如果数字恰好在中间,则将其舍入为具有偶数最低有效位的最近值。

但这显然不是round half to even,一些数字被舍入为奇数。

我们如何解释 .NET Framework 4.7.2 和 .NET 6 中的差异,并如何在 .NET 6 中以与 .NET Framework 相同的方式舍入数字?


3
我非常怀疑问题出在{1:F1}这部分,但现在已经很晚了(即睡觉时间),我无法为您调查。 - Andrew Morton
6
也许行为上的差异与.NET Core 3.0中浮点数格式化“改进”有关。 - Michael Liu
3
尝试将测试更改为格式说明符,例如{1:F20},看看会发生什么。快速浏览@MichaelLiu的链接让我相信这些更改是你所看到结果的原因。记住:floatdouble是不精确的表示。 - Flydog57
2
@CarlWitthoft 它被称为“银行家舍入法”,我认为.NET也使用这个规则,如果你看到了David Browne的答案。 - obnews
3
Windows .NET 更新后最后一位数字的舍入方式发生了变化,.Net Core 3.1 与 .Net Core 2.0/.Net Framework 的舍入问题,以及 .NET 5 和 .NET 4.7.2 中的舍入差异。 - phuclv
显示剩余7条评论
2个回答

53

使用十进制而不是双精度浮点数,否则你的起始值可能与你预期的不完全一致,从而导致结果出现偏差。

9.05 ===> 9.1
9.15 ===> 9.2
9.25 ===> 9.3
9.35 ===> 9.4
9.45 ===> 9.5
9.55 ===> 9.6
9.65 ===> 9.7
9.75 ===> 9.8
9.85 ===> 9.9
9.95 ===> 10.0

使用双精度浮点数时,大多数值与代码中的十进制文字略有偏差,因此会被舍入为最接近的数字。只有两个值实际上处于中点位置,并在.NET Core中向偶数舍入。但是正如@Traveller指出的那样,这不是一般舍入行为;它是特定于浮点数打印方式的。

9.05000000000000071E+000 ===> 9.1 <- rounded to nearest
9.15000000000000036E+000 ===> 9.2 <- rounded to nearest
9.25000000000000000E+000 ===> 9.2 <- rounded to even
9.34999999999999964E+000 ===> 9.3 <- rounded to nearest
9.44999999999999929E+000 ===> 9.4 <- rounded to nearest
9.55000000000000071E+000 ===> 9.6 <- rounded to nearest
9.65000000000000036E+000 ===> 9.7 <- rounded to nearest
9.75000000000000000E+000 ===> 9.8 <- rounded to even
9.84999999999999964E+000 ===> 9.8 <- rounded to nearest
9.94999999999999929E+000 ===> 9.9 <- rounded to nearest

10
这似乎不能解释问题中观察到的差异。改变变量类型是作弊的 ;) 有官方文档支持这种更改吗? - Andrew Morton
28
我不同意 @AndrewMorton 的看法。如果代码操作十进制数值,例如 9.25,则建议更改使用的类型是正确的回答。 - tymtam
6
@AndrewMorton 使用双精度浮点数并期望得到精确结果是错误的。如果您需要以受控的方式四舍五入,请使用双精度浮点数。https://dev59.com/G3RB5IYBdhLWcg3wj36c - mmmmmm
3
任何其他数字都比9.25更好作为例子。 因为9.25可以表示为分母为2的幂次方的分数,所以它可以被精确地表示为二进制浮点数"double"。(在https://www.h-schmidt.net/FloatConverter/IEEE754.html中尝试单精度浮点数) 。像9.15这样的源值是一个问题,因为您可以在David的表格中看到,最接近于9.15、9.25的双精度浮点数所表示的十进制值。(即应用于编译时文本代码的四舍五入结果,然后在转换为字符串时进行运行时舍入。) - Peter Cordes
1
@PeterCordes 哈哈,这是什么概率啊。我猜,概率应该是20%,因为9.75是另一个完全代表数字。 - tymtam
显示剩余4条评论

38
微软文档中将此信息小心地隐藏在标准数值格式字符串页面中(也可能在其他地方,但不在Double.ToString文档中)。
以下是重要的摘录,供后人参考:
当精度说明符控制结果字符串中小数位数时,结果字符串反映了四舍五入到最接近无限精确结果的可表示结果。如果有两个相等的可表示结果:
  • 在.NET Framework和.NET Core 2.0之前的.NET Core上,运行时选择具有更大的最低有效数字的结果(即使用MidpointRounding.AwayFromZero)。

  • 在.NET Core 2.1及更高版本上,运行时选择具有偶数最低有效数字的结果(即使用MidpointRounding.ToEven)。

由于.NET 5及更高版本基本上继续了Core系列,尽管Microsoft关于它们已经合并的混乱声明,这很明显属于第二种情况。

15
еӣ дёә9.05000000000000071054жҜ”9.0жӣҙжҺҘиҝ‘дәҺ9.1гҖӮеҸӘжңүеҪ“ж•°еӯ—зЎ®е®һеӨ„дәҺдёӯй—ҙдҪҚзҪ®ж—¶пјҢдҫӢеҰӮ9.25000000000000000пјҢMidpointRoundingжүҚдјҡеә”з”ЁгҖӮ - David Browne - Microsoft
9
@obnews - "向远离零的方向取整"并仅使用正数(例如从学校中记得的四舍五入)的一个问题是,您不会意识到只有当您恰好在中点时才适用舍入规则。您无需调用舍入规则就能知道9.599更接近10。因此,当您使用不同的舍入规则时,仍不会将9.59舍入为9 - Damien_The_Unbeliever
2
Math.Round页面上有更多关于.NET舍入的信息。 - molnarm
1
@Damien_The_Unbeliever:C#是否允许您设置像IEEE roundTowardNegative这样的舍入模式,以便9.99四舍五入为9,-9.01四舍五入为-10?(向下舍入,朝向负无穷大)。半相关:x86硬件直接支持的四种舍入模式(对于每个数学运算和舍入到整数)是默认的roundTiesToEven(C# MidpointRounding.ToEven),roundTowardPositive(ceil),roundTowardNegative(floor)和roundTowardZero(trunc)。无论如何,如果使用非中点舍入规则进行浮点数->字符串转换(在此过程中SW必须完成工作),这可能很重要。 - Peter Cordes
@Peter C#支持以下舍入模式。如果我没记错的话,银行家舍入是默认值。 - Voo
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接