为什么 .NETdecimal.ToString(string) 从零舍入,显然与语言规范不一致?
我发现,在 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 todVal.ToString(sFmt)
, will result in a string containing a rounded version ofdVal
...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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
如果你仔细阅读规范,你会发现这里没有不一致的地方。
这是该段落,突出显示了重要部分:
规范的这一部分适用于
十进制
上的算术运算;字符串格式不是其中之一,即使是,也没关系,因为您的示例是低精度的。要演示规范中提到的行为,请使用以下代码:
这就是规范所讨论的全部内容。如果某些计算的结果超出
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:
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:
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.ToString()
默认格式根据Culture
,而不是根据规范的计算方面。显然,您所在区域的Culture
(从外观来看,大多数情况下)都期望从零舍入。如果您想要不同的行为,您可以传递一个IFormatProvider
到ToString()
我想到了上面的内容,但你是对的,无论
文化
如何,它总是从零舍入。正如对此答案的评论所链接的, 这里(MS Docs)是有关该行为的官方文档。从该链接页面的顶部摘录,重点关注最后两个列表项:
至于你的问题——
--- 从 Framework 到 Core 2.1+ 的行为变化所暗示的答案可能是,“不,没有充分的理由,所以我们 (Microsoft) 继续使运行时与 .NET Core 2.1 中的语言保持一致,并且之后。”
ToString()
by default formats according to theCulture
, not according to a computational aspect of the specification. Apparently theCulture
for your locale (and most, from the looks of it) expects rounding away from zero.If you want different behavior, you can pass anIFormatProvider
in toToString()
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:
As far as your question ---
--- 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."
很可能是因为这是处理货币的标准方式。创建小数的动力是浮点在处理货币价值方面表现不佳,因此您会期望它的规则比数学正确性更符合会计标准。
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.