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

sta*_*ack 18 c# decimal tostring rounding number-formatting

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

  • 一个 decimal dVal
  • string sFmt传入的格式dVal.ToString(sFmt)将导致包含圆形版本的字符串dVal

...很明显,decimal.ToString(string)返回使用舍入的值MidpointRounding.AwayFromZero.这似乎是C#规范的直接矛盾.

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

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

示例代码:

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();
}
Run Code Online (Sandbox Code Playgroud)


C#语言规范4.1.7节中的段落("小数类型")(在此处获取完整规范(.doc)):

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

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

Aar*_*ght 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);
Run Code Online (Sandbox Code Playgroud)

这就是所有规范都在谈论的内容.如果某些计算的结果超过了decimal类型的精度限制(29位),则使用银行家的舍入来确定结果的结果.


Mic*_*ene 5

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+ 的行为变化所暗示的答案可能是,“不,没有充分的理由,所以我们 (Microsoft) 继续使运行时与 .NET Core 2.1 中的语言保持一致,并且之后。”