我看到,在C#中,默认情况下舍入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节中的段落(“小数类型”)(获取完整规范here(。doc)):
对十进制类型值的操作结果是计算精确结果(保留每个运算符定义的比例)然后舍入以适合表示形式的结果。结果四舍五入到最接近的可表示值,并且当结果等于两个可表示的值时,结果四舍五入到在最低有效数字位置具有偶数的值(这称为“银行家舍入”)。零结果的符号始终为0,标度为0。
很容易看出他们可能没有在本段中考虑ToString(string)
,但我倾向于认为它符合此描述。
答案 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)
最有可能的原因是这是处理货币的标准方式。创建小数的动力是浮点在处理货币值方面做得不好,所以你会期望它的规则与会计标准更加一致而不是数学正确性。