为什么Math.Round(2.5)返回2而不是3?

481

在C#中,Math.Round(2.5)的结果是2。

这不应该是3吗?为什么在C#中结果却是2呢?


7
这实际上是一项功能。请参阅<a href="http://msdn.microsoft.com/en-us/library/3s2d3xkk(VS.80).aspx">MSDN文档</a>。这种取整方式称为“银行家舍入法”。至于解决方法,有<a href="http://msdn.microsoft.com/en-us/library/ms131274(VS.80).aspx">一个重载</a>允许调用者指定如何进行取整。 - Joe
1
显然,当要求将一个数字精确地四舍五入到两个整数之间时,round方法会返回偶数。因此,Math.Round(3.5)返回4。请参阅此文章:http://msdn.microsoft.com/en-us/library/3s2d3xkk.aspx - Matthew Jones
36
Math.Round(2.5, 0, MidpointRounding.AwayFromZero); - Robert Durgin
10
@amed 这不是一个 bug,这是二进制浮点数的工作方式。“1.005” 无法在双精度下准确表示。它可能是“1.00499...”。如果使用 Decimal 类型,这个问题就会消失。在我的看法中,Math.Round 方法重载,以双精度数字的小数位数为参数,是一个值得怀疑的设计选择,因为它很少能够有效地工作。 - CodesInChaos
@ColeJohnson:IEEE规定四舍五入到偶数。由于它可以被准确地表示,因此应用了这个规则。 - leppie
显示剩余6条评论
15个回答

623
首先,这不是C#的错误——这是.NET的错误。C#是一种语言——它不决定如何实现Math.Round
其次,如果你阅读文档,你会发现默认的四舍五入是"银行家舍入"(偶数舍):
返回值:类型:System.Double a最接近的整数。如果a的小数部分恰好在两个整数之间,其中一个是偶数,另一个是奇数,则返回偶数。请注意,此方法返回Double而不是整数类型。
备注:此方法的行为遵循IEEE标准754第4节。这种舍入有时被称为最近舍入或银行家舍入。它最小化了由于在单个方向上一致地舍入中点值而导致的舍入误差。
你可以使用重载来指定Math.Round如何舍入中点,该重载采用MidpointRounding值。每个没有MidpointRounding的重载都有一个相应的带有MidpointRounding的重载。 无论这个默认值是否选择得当都是另一回事。(在.NET 2.0中才引入了MidpointRounding。在此之前,我不确定有没有任何简单的方法实现所需的行为,除非自己动手做。)特别是历史已经证明这不是预期的行为,在大多数情况下,这是API设计中的致命错误。我可以看出银行家舍入为什么有用......但对许多人来说,这仍然是一个惊喜。
您可能会对查看最近的Java等效枚举(RoundingMode)感兴趣,它提供了更多选项。(它不仅处理中点。)

5
我不确定这是否是一个 bug,我认为这是设计意图,因为 0.5 与最接近的较小整数和最接近的较大整数的距离相同。 - Stan R.
4
在.NET出现之前,我记得在VB中出现过这种行为。 - John Fiala
9
的确,正如文档所述,IEEE标准754第4节。 - Jon Skeet
2
我之前也曾因此而受挫,认为这纯属疯狂。幸运的是,他们添加了一种指定我们在小学就学过的四舍五入方式的方法:MidPointRounding。 - Shea
34
对于“这不是预期行为[...],这是API设计中的致命问题”的观点表示赞同。+1 - BlueRaja - Danny Pflughoeft
显示剩余2条评论

247
那被称为"四舍六入五凑偶"(又称银行家舍入)的方法,是最小化加和误差的一种有效舍入策略 (MidpointRounding.ToEven) 。理论上,如果您总是将0.5舍入到同一个方向,误差会更快地累积(四舍六入五凑偶可最小化这种误差)(a)
请查看以下链接获取MSDN中的详细描述:
- Math.Floor,向下舍入到负无穷大的值。 - Math.Ceiling,向上舍入到正无穷大的值。 - Math.Truncate,向零舍入。 - Math.Round,舍入到最接近的整数或指定的小数位数。您可以指定在恰好处于两个可能性之间时的行为,例如四舍五入使得结果的个位数为偶数 ("Round(2.5,MidpointRounding.ToEven)"将成为2),或使结果远离零 ("Round(2.5,MidpointRounding.AwayFromZero)"将成为3)。
以下图表或许可以帮到您:
-3        -2        -1         0         1         2         3
 +--|------+---------+----|----+--|------+----|----+-------|-+
    a                     b       c           d            e

                       a=-2.7  b=-0.5  c=0.3  d=1.5  e=2.8
                       ======  ======  =====  =====  =====
Floor                    -3      -1      0      1      2
Ceiling                  -2       0      1      2      3
Truncate                 -2       0      0      1      2
Round(ToEven)            -3       0      0      2      3
Round(AwayFromZero)      -3      -1      0      2      3

请注意,Round函数比看起来更加强大,因为它可以四舍五入到特定的小数位数。而其他函数总是向零舍入。例如:

n = 3.145;
a = System.Math.Round (n, 2, MidpointRounding.ToEven);       // 3.14
b = System.Math.Round (n, 2, MidpointRounding.AwayFromZero); // 3.15

对于其他函数,你需要使用乘法/除法技巧来实现相同的效果:

c = System.Math.Truncate (n * 100) / 100;                    // 3.14
d = System.Math.Ceiling (n * 100) / 100;                     // 3.15

(a) 当然,这种理论取决于你的数据在偶数部分(0.5、2.5、4.5等)和奇数部分(1.5、3.5等)之间具有相当均匀的值分布。

如果所有“半值”都是偶数(例如),错误将会像总是向上舍入一样快速积累。


3
也被称为银行家舍入。 - Pondidum
好的解释!我想亲自看看错误是如何累积的,于是我编写了一个脚本来展示使用银行家舍入法舍入的值,在长期内,其总和和平均值与原始值非常接近。https://github.com/AmadeusW/RoundingDemo(有绘图图片可用) - Amadeusz Wieczorek
刚过了一会儿:e的刻度(=2.8)不应该比2的刻度更靠右吗? - superjos
一种简单的记忆方式,假设十分位为5:
  • 个位和十分位都是奇数=向上取整
  • 个位和十分位不同=向下取整
  • 零不是奇数
  • 对于负数反转
- Arkham Angel
@ArkhamAngel,实际上这似乎比“使最后一位数字为偶数”更难记住 :-) - paxdiablo
显示剩余2条评论

48

您应该查看MSDN关于 Math.Round 的内容:

此方法的行为遵循IEEE标准754第4节。这种舍入有时被称为最近舍入或银行家舍入。

您可以使用重载来指定 Math.Round 的行为:

Math.Round(2.5, 0, MidpointRounding.AwayFromZero); // gives 3

Math.Round(2.5, 0, MidpointRounding.ToEven); // gives 2

47

MSDN, Math.Round(double a) 返回:

a 最近的整数。如果 a 的小数部分恰好在两个整数中间,其中一个是偶数,另一个是奇数,则返回偶数。

...因此,2.5作为介于2和3之间的数字会向下舍入到偶数(2),这被称为银行家舍入(或向最近偶数舍入),并且是常用的舍入标准之一。

同一 MSDN 文章:

此方法的行为遵循 IEEE 标准 754 第4节。这种舍入有时被称为最近取整,或者银行家舍入。它最小化了由于在单一方向上一致地将中点值舍入而导致的舍入误差。

您可以通过调用采用 MidpointRounding 模式的 Math.Round 的重载来指定不同的舍入行为。


34

舍入的本质

考虑将一个含有小数部分的数字舍入到整数,例如舍入为一个整数。在这种情况下,舍入的过程是确定哪个整数最好地代表您要舍入的数字。

在常见的或称为“算术”舍入中,很明显2.1、2.2、2.3和2.4舍入为2.0;而2.6、2.7、2.8和2.9则舍入为3.0。

这就留下了2.5,它与2.0和3.0一样接近。你需要在2.0和3.0之间做出选择,任何一个都是同样有效的。

对于负数,-2.1、-2.2、-2.3和-2.4将变为-2.0;而-2.6、-2.7、-2.8和-2.9在算术舍入中将变为-3.0。

对于-2.5,则需要在-2.0和-3.0之间进行选择。

其他形式的舍入

“向上取整”将带有小数位的任何数字都变成下一个“整数”。因此,不仅2.5和2.6舍入为3.0,2.1和2.2也是如此。

向上取整会使正数和负数离开零点。例如,2.5会变成3.0,-2.5会变成-3.0。

“向下取整”通过截断不需要的数字来截短数字。这将使数字朝零点移动。例如,2.5变成2.0,-2.5变成-2.0。

在“银行家舍入法”中,通常情况下会将待舍入的0.5四舍五入为最接近的偶数。例如2.5将被舍入为2.0,3.5将被舍入为4.0,4.5将被舍入为4.0,5.5将被舍入为6.0等。
而“交替舍入法”则会对任何一个0.5交替地向上或向下舍入。
“随机舍入法”会完全随机地向上或向下舍入一个0.5。
舍入函数可以分为“对称”和“非对称”两种类型。当一个舍入函数把所有数值无论正负都舍入到离其最近的整数时,这个函数就被称为“对称”的。反之,如果该函数将正数舍入到零,而将负数舍入到远离零的位置,则被称为“非对称”功能,例如2.5舍入为2.0;-2.5舍入为-3.0。还有一种“非对称”函数是将正数舍入到远离零的位置,而将负数舍入到零附近的位置,例如2.5舍入为3.0;-2.5舍入为-2.0。
大多数情况下,人们认为采用对称舍入,例如在C#中使用Round(AwayFromZero)函数,-2.5将被舍入为-3.0,而3.5将被舍入为4.0。

31

默认的MidpointRounding.ToEven或银行家舍入(2.5变为2,4.5变为4等)曾经让我在编写会计报告时感到困扰,因此我将写下我之前发现的一些内容,并从本文中了解更多。

这些舍入偶数的银行家是谁(也许是英国银行家!)?

来自维基百科

“银行家舍入”的起源仍然很模糊。如果这种舍入方法曾经是银行业的标准,那么很难找到证据。相反,欧洲委员会报告《欧元的引入和货币金额的四舍五入》第2节指出,在银行业中先前没有任何标准的舍入方法;并且它规定“半途”金额应向上舍入。

对于银行而言,这似乎是一种非常奇怪的舍入方式,除非银行曾经接收过许多相同金额的存款。 比如:存入240万英镑,但我们称其为200万英镑先生。

IEEE 754标准可追溯至1985年,并提供两种舍入方式,但建议使用银行家舍入。 这个维基百科文章列出了语言如何实现舍入的方法(如果以下任何一种不正确,请纠正我),大多数语言都没有使用银行家舍入,而是你在学校里学到的舍入方法:

  • C/C++中的round()函数来自math.h将数字四舍五入到最接近的整数(不是银行家舍入)
  • JavaMath.Round方法将数值四舍五入(向下取整,加上0.5,转换为整数)。在BigDecimal中也有一种替代方法。
  • Perl与C类似。
  • Javascript 与 Java 的 Math.Round 方法相同。

谢谢提供这些信息。我之前从未意识到这一点。虽然您提到的关于数百万美元的例子可能有点夸张,但即使在分角时四舍五入,如果所有半分都向上取整,那么要在1000万个银行账户上支付利息将会给银行带来很大的成本,反之如果所有半分都向下取整,则会让客户付出很大代价。因此,我可以想象这是一个公认的标准。不确定银行家是否真正使用了这种方式。大多数客户不会注意到四舍五入,这会带来很多收益,但如果您生活在一个以客户为中心的国家,这很可能是法律所规定的。 - Harald Coppoolse

15

来自MSDN:

默认情况下,Math.Round使用MidpointRounding.ToEven。大多数人对“舍入至偶数”不熟悉,而“远离零点四舍五入”则更常在学校教授。.NET默认使用“舍入至偶数”,因为从统计学角度上讲它更为优越,即使被舍入的数字倾向于正数也不会像“远离零点舍入”那样更容易出现向上舍入多于向下舍入的情况。

http://msdn.microsoft.com/en-us/library/system.math.round.aspx


3

由于Silverlight不支持MidpointRounding选项,您需要编写自己的选项。例如:

public double RoundCorrect(double d, int decimals)
{
    double multiplier = Math.Pow(10, decimals);

    if (d < 0)
        multiplier *= -1;

    return Math.Floor((d * multiplier) + 0.5) / multiplier;

}

有关如何将其用作扩展的示例,请参阅文章:.NET和Silverlight舍入


3

我曾经遇到一个问题,我的SQL服务器将0.5四舍五入为1,而我的C#应用程序没有这样做。因此,你会看到两个不同的结果。

以下是使用int/long实现的方法。这就是Java的四舍五入方式。

int roundedNumber = (int)Math.Floor(d + 0.5);

这可能也是您能想到的最有效的方法。

如果您想保留双精度并使用小数精度,则只需要根据小数点后面的位数使用以10为底数的指数即可。

public double getRounding(double number, int decimalPoints)
{
    double decimalPowerOfTen = Math.Pow(10, decimalPoints);
    return Math.Floor(number * decimalPowerOfTen + 0.5)/ decimalPowerOfTen;
}

您可以输入负小数来表示小数点,同样也可以正常工作。

getRounding(239, -2) = 200

0

简单的方法是:

Math.Ceiling(decimal.Parse(yourNumber + ""));

你知道强制类型转换的概念吗? - Kaspars Ozols

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