21

我看到,在 C# 中,将 a 舍入decimal,默认情况下使用MidpointRounding.ToEven. 这是意料之中的,也是 C# 规范所规定的。但是,鉴于以下情况:

  • 一种decimal dVal
  • 一种格式string sFmt,当传入 时dVal.ToString(sFmt),将产生一个包含四舍五入版本的字符串dVal

...很明显,decimal.ToString(string)返回一个使用MidpointRounding.AwayFromZero. 这似乎与 C# 规范直接矛盾。

我的问题是:这种情况有充分的理由吗?或者这只是语言的不一致?

下面,作为参考,我包含了一些代码,这些代码写入控制台各种舍入运算结果和decimal.ToString(string)运算结果,每个结果都针对值数组中的每个decimal值。实际输出是嵌入的。之后,我在decimal类型的 C# 语言规范部分包含了一个相关段落。

示例代码:

static void Main(string[] args)
{
    decimal[] dArr = new decimal[] { 12.345m, 12.355m };

    OutputBaseValues(dArr);
    // Base values:
    // d[0] = 12.345
    // d[1] = 12.355

    OutputRoundedValues(dArr);
    // Rounding with default MidpointRounding:
    // Math.Round(12.345, 2) => 12.34
    // Math.Round(12.355, 2) => 12.36
    // decimal.Round(12.345, 2) => 12.34
    // decimal.Round(12.355, 2) => 12.36

    OutputRoundedValues(dArr, MidpointRounding.ToEven);
    // Rounding with mr = MidpointRounding.ToEven:
    // Math.Round(12.345, 2, mr) => 12.34
    // Math.Round(12.355, 2, mr) => 12.36
    // decimal.Round(12.345, 2, mr) => 12.34
    // decimal.Round(12.355, 2, mr) => 12.36

    OutputRoundedValues(dArr, MidpointRounding.AwayFromZero);
    // Rounding with mr = MidpointRounding.AwayFromZero:
    // Math.Round(12.345, 2, mr) => 12.35
    // Math.Round(12.355, 2, mr) => 12.36
    // decimal.Round(12.345, 2, mr) => 12.35
    // decimal.Round(12.355, 2, mr) => 12.36

    OutputToStringFormatted(dArr, "N2");
    // decimal.ToString("N2"):
    // 12.345.ToString("N2") => 12.35
    // 12.355.ToString("N2") => 12.36

    OutputToStringFormatted(dArr, "F2");
    // decimal.ToString("F2"):
    // 12.345.ToString("F2") => 12.35
    // 12.355.ToString("F2") => 12.36

    OutputToStringFormatted(dArr, "###.##");
    // decimal.ToString("###.##"):
    // 12.345.ToString("###.##") => 12.35
    // 12.355.ToString("###.##") => 12.36

    Console.ReadKey();
}

private static void OutputBaseValues(decimal[] dArr)
{
    Console.WriteLine("Base values:");
    for (int i = 0; i < dArr.Length; i++) Console.WriteLine("d[{0}] = {1}", i, dArr[i]);
    Console.WriteLine();
}

private static void OutputRoundedValues(decimal[] dArr)
{
    Console.WriteLine("Rounding with default MidpointRounding:");
    foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2) => {1}", d, Math.Round(d, 2));
    foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2) => {1}", d, decimal.Round(d, 2));
    Console.WriteLine();
}

private static void OutputRoundedValues(decimal[] dArr, MidpointRounding mr)
{
    Console.WriteLine("Rounding with mr = MidpointRounding.{0}:", mr);
    foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2, mr) => {1}", d, Math.Round(d, 2, mr));
    foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2, mr) => {1}", d, decimal.Round(d, 2, mr));
    Console.WriteLine();
}

private static void OutputToStringFormatted(decimal[] dArr, string format)
{
    Console.WriteLine("decimal.ToString(\"{0}\"):", format);
    foreach (decimal d in dArr) Console.WriteLine("{0}.ToString(\"{1}\") => {2}", d, format, d.ToString(format));
    Console.WriteLine();
}


C# 语言规范(“十进制类型”)第 4.1.7 节中的段落(在此处获取完整规范(.doc)):

对十进制类型的值进行运算的结果是计算精确结果(保留为每个运算符定义的比例)然后四舍五入以适应表示的结果。结果四舍五入到最接近的可表示值,并且当结果同样接近两个可表示值时,四舍五入到在最低有效数字位置具有偶数的值(这称为“银行家四舍五入”)。零结果的符号始终为 0,标度为 0。

很容易看出他们可能没有ToString(string)在这一段中考虑过,但我倾向于认为它符合这个描述。

4

3 回答 3

6

如果您仔细阅读规范,您会发现这里没有不一致之处。

这又是该段,突出显示了重要部分:

对十进制类型的值进行运算的结果是通过计算精确结果(保留为每个运算符定义的比例)然后四舍五入以适应表示的结果。结果四舍五入到最接近的可表示值,并且当结果同样接近两个可表示值时,四舍五入到在最低有效数字位置具有偶数的值(这称为“银行家四舍五入”)。零结果的符号始终为 0,标度为 0。

规范的这一部分适用于;上的算术运算。decimal字符串格式不是其中之一,即使是,也没关系,因为您的示例是低精度的。

要演示规范中提到的行为,请使用以下代码:

Decimal d1 = 0.00000000000000000000000000090m;
Decimal d2 = 0.00000000000000000000000000110m;

// Prints: 0.0000000000000000000000000004 (rounds down)
Console.WriteLine(d1 / 2);

// Prints: 0.0000000000000000000000000006 (rounds up)
Console.WriteLine(d2 / 2);

这就是规范所讨论的全部内容。如果某些计算的结果将超过decimal类型的精度限制(29 位),则使用银行家四舍五入来确定结果将是什么。

于 2010-02-12T03:54:20.650 回答
3

ToString()默认情况下根据 的格式Culture,而不是根据规范的计算方面。显然,Culture对于您的语言环境(从外观上看,大多数情况下)都希望从零四舍五入。

如果你想要不同的行为,你可以传递IFormatProvider一个ToString()

我想到了上述内容,但你是对的,它总是从零开始四舍五入,无论Culture.


正如对此答案的评论所链接的那样,此处(MS Docs)是有关该行为的官方文档。摘自该链接页面的顶部,并重点关注最后两个列表项:

标准数字格式字符串用于格式化常见的数字类型。标准数字格式字符串采用 形式Axx,其中:

  • A是一个称为格式说明符的单个字母字符。任何包含多个字母字符(包括空格)的数字格式字符串都被解释为自定义数字格式字符串。有关详细信息,请参阅自定义数字格式字符串

  • xx是一个可选整数,称为精度说明符。精度说明符的范围从 0 到 99,并影响结果中的位数。请注意,精度说明符控制数字字符串表示中的位数。它不会对数字本身进行四舍五入。要执行舍入运算,请使用Math.CeilingMath.FloorMath.Round方法。

    精度说明符控制结果字符串中的小数位数时,结果字符串反映了一个数字,该数字四舍五入为最接近无限精确结果的可表示结果。如果有两个同样接近的可表示结果:

    • 在 .NET Framework 和 .NET Core 直到 .NET Core 2.0上,运行时选择具有较大最低有效数字的结果(即,使用MidpointRounding.AwayFromZero)。

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


至于你的问题---

这种情况有充分的理由吗?或者这只是语言的不一致?

--- 从 Framework 到 Core 2.1+ 的行为变化所暗示的答案可能是,“不,没有充分的理由,所以我们(微软)继续并让运行时与 .NET Core 2.1 中的语言保持一致,并且之后。”

于 2010-02-12T02:24:24.927 回答
1

很可能是因为这是处理货币的标准方式。创造小数的动力是浮点在处理货币价值方面做得很差,所以你会期望它的规则更符合会计标准而不是数学正确性。

于 2010-02-12T02:27:23.990 回答