为什么.NET decimal.ToString(string)从零开始,显然与语言规范不一致?

时间:2010-02-12 02:20:56

标签: c# decimal tostring number-formatting rounding

我看到,在C#中,默认情况下舍入decimal会使用MidpointRounding.ToEven。这是预期的,也是C#规范所要求的。但是,考虑到以下因素:

  • A 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节中的段落(“小数类型”)(获取完整规范here(。doc)):

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

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

3 个答案:

答案 0 :(得分: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位)的精度限制,则使用银行家的舍入来确定结果的结果。

答案 1 :(得分:1)

默认情况下,

ToString()根据Culture格式,而不是根据规范的计算方面。显然,您的语言环境的Culture(以及大多数情况下,从它的外观)预计会从零开始四舍五入。

如果您想要不同的行为,可以将IFormatProvider传递给ToString()

我想到了上述情况,但你是正确的,无论Culture如何,它总是从零开始。检查注释中的链接以获取证明。

答案 2 :(得分:1)

最有可能的原因是这是处理货币的标准方式。创建小数的动力是浮点在处理货币值方面做得不好,所以你会期望它的规则与会计标准更加一致而不是数学正确性。