为什么 .NETdecimal.ToString(string) 从零舍入,显然与语言规范不一致?

发布于 2024-08-21 04:25:19 字数 3900 浏览 6 评论 0原文

我发现,在 C# 中,默认情况下,对小数进行四舍五入使用MidpointRounding.ToEven。这是预期的,也是 C# 规范所规定的。但是,给定以下条件:

  • decimal dVal
  • 格式 string sFmt,当传递给 dVal.ToString(sFmt) 时,将导致包含 dVal 舍入版本的字符串

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

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

下面,作为参考,我包含了一些代码,这些代码写入控制台各种舍入运算结果和decimal.ToString(string) 运算结果,每个结果针对十进制数组中的每个值值。实际输出是嵌入的。之后,我添加了 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();
}


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

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

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

I see that, in C#, rounding a decimal, by default, uses MidpointRounding.ToEven. This is expected, and is what the C# spec dictates. However, given the following:

  • A decimal dVal
  • A format string sFmt that, when passed in to dVal.ToString(sFmt), will result in a string containing a rounded version of dVal

...it is apparent that decimal.ToString(string) returns a value rounded using MidpointRounding.AwayFromZero. This would appear to be a direct contradiction of the C# spec.

My question is this: is there a good reason this is the case? Or is this just an inconsistency in the language?

Below, for reference, I've included some code that writes to console an assortment of rounding operation results and decimal.ToString(string) operation results, each on every value in an array of decimal values. The actual outputs are embedded. After that, I've included a relevant paragraph from the C# Language Specification section on the decimal type.

The example code:

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();
}

The paragraph from section 4.1.7 of the C# Language Specification ("The decimal type") (get the full spec here (.doc)):

The result of an operation on values of type decimal is that which would result from calculating an exact result (preserving scale, as defined for each operator) and then rounding to fit the representation. Results are rounded to the nearest representable value, and, when a result is equally close to two representable values, to the value that has an even number in the least significant digit position (this is known as “banker’s rounding”). A zero result always has a sign of 0 and a scale of 0.

It's easy to see that they may not have been considering ToString(string) in this paragraph, but I'm inclined to think it fits in this description.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

淡莣 2024-08-28 04:25:19

如果你仔细阅读规范,你会发现这里没有不一致的地方。

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

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

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

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

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位),则使用银行四舍五入来确定结果。

If you read the spec carefully, you will see that there is no inconsistency here.

Here's that paragraph again, with the important parts highlighted:

The result of an operation on values of type decimal is that which would result from calculating an exact result (preserving scale, as defined for each operator) and then rounding to fit the representation. Results are rounded to the nearest representable value, and, when a result is equally close to two representable values, to the value that has an even number in the least significant digit position (this is known as “banker’s rounding”). A zero result always has a sign of 0 and a scale of 0.

This part of the spec applies to arithmetic operations on decimal; string formatting is not one of those, and even if it were, it wouldn't matter because your examples are low-precision.

To demonstrate the behaviour referred to in the spec, use the following code:

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);

That's all the spec is talking about. If the result of some calculation would exceed the precision limit of the decimal type (29 digits), banker's rounding is used to determine what the result will be.

风透绣罗衣 2024-08-28 04:25:19

ToString() 默认格式根据 Culture,而不是根据规范的计算方面。显然,您所在区域的Culture(从外观来看,大多数情况下)都期望从零舍入。

如果您想要不同的行为,您可以传递一个IFormatProviderToString()

我想到了上面的内容,但你是对的,无论文化如何,它总是从零舍入。


正如对此答案的评论所链接的, 这里(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 中的语言保持一致,并且之后。”

ToString() by default formats according to the Culture, not according to a computational aspect of the specification. Apparently the Culture for your locale (and most, from the looks of it) expects rounding away from zero.

If you want different behavior, you can pass an IFormatProvider in to ToString()

I thought the above, but you are correct that it always rounds away from zero no matter the Culture.


As also linked by a comment on this answer, here (MS Docs) is official documentation on the behavior. Excerpting from the top of that linked page, and focusing on the last two list items:

Standard numeric format strings are used to format common numeric types. A standard numeric format string takes the form Axx, where:

  • A is a single alphabetic character called the format specifier. Any numeric format string that contains more than one alphabetic character, including white space, is interpreted as a custom numeric format string. For more information, see Custom Numeric Format Strings.

  • xx is an optional integer called the precision specifier. The precision specifier ranges from 0 to 99 and affects the number of digits in the result. Note that the precision specifier controls the number of digits in the string representation of a number. It does not round the number itself. To perform a rounding operation, use the Math.Ceiling, Math.Floor, or Math.Round method.

    When precision specifier controls the number of fractional digits in the result string, the result string reflects a number that is rounded to a representable result nearest to the infinitely precise result. If there are two equally near representable results:

    • On the .NET Framework and .NET Core up to .NET Core 2.0, the runtime selects the result with the greater least significant digit (that is, using MidpointRounding.AwayFromZero).

    • On .NET Core 2.1 and later, the runtime selects the result with an even least significant digit (that is, using MidpointRounding.ToEven).


As far as your question ---

Is there a good reason this is the case? Or is this just an inconsistency in the language?

--- the answer implied by the change in behavior from Framework to Core 2.1+ is possibly, "No, there was no good reason, so we (Microsoft) went ahead and made the runtime consistent with the language in .NET Core 2.1 and later."

小瓶盖 2024-08-28 04:25:19

很可能是因为这是处理货币的标准方式。创建小数的动力是浮点在处理货币价值方面表现不佳,因此您会期望它的规则比数学正确性更符合会计标准。

Most likely because this is the standard way of dealing with currency. The impetus for the creation of decimal was that floating point does a poor job of dealing with currency values, so you would expect it's rules to be more aligned with accounting standards than mathematical correctness.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文