关于C#:财务计算: double 还是decimal?

Financial calculations: double or decimal?

本问题已经有最佳答案,请猛点这里访问。

我们正在进行财务计算。我发现了一篇关于将货币值存储为小数的文章:十进制与双倍!-我应该用哪一个?什么时候用?

所以我把数量存储为小数。

我有以下计算:12.000*(1/12)=1.000

如果使用decimal数据类型存储金额和结果金额,则不会得到预期的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// First approach:    
decimal ratio = 1m / 12m;
decimal amount = 12000;
decimal ratioAmount = amount * ratio;
ratioAmount = 999.9999999999999

// Second approach:
double ratio = 1d / 12d;
decimal amount = 12000;
decimal ratioAmount = (decimal)((double)amount * ratio);
ratioAmount = 1.000

// Third approach:
double ratio = 1d / 12d;
double amount = 12000;
double ratioAmount = amount * ratio;
ratioAmount = 1.000

最好的方法是什么?每个人都在谈论金额/货币必须以小数形式存储。


从不,永远,永远,永远,永远,永远不要把财务金额存储在一个双倍。以下是我博客中的一个例子,说明了为什么不应该使用double

1
2
3
4
5
6
7
8
9
10
11
12
13
var lineValues = new List<double> { 1675.89, 2600.21, 5879.79, 5367.51, 8090.30, 492.97, 7888.60 };
double dblAvailable = 31995.27d;
double dblTotal = 0d;

foreach (var lineValue in lineValues)
{
    dblTotal += lineValue;
}

if (dblAvailable < dblTotal)
{
    Console.WriteLine("They don't add up!");
}

你会看到,因为双打实际上加起来是31995.270000000004,所以Console.WriteLine将会被击中。正如您可以从变量的名称猜到的那样,这个代码示例是基于财务系统中的一些实际代码——这个问题导致用户无法正确地将金额分配给交易。

用这个附加代码将数字加起来作为decimals:

1
2
3
4
5
6
7
decimal decAvailable = (decimal)dblAvailable;
decimal decTotal = (decimal)dblTotal;

if (decAvailable < decTotal)
{
    Console.WriteLine("They still don't add up!");
}

不会打到Console.WriteLine。这个故事的寓意是:使用decimal进行财务计算!

十进制关键字的语言引用的第一部分说明:

Compared to other floating-point types, the decimal type has more precision and a smaller range, which makes it appropriate for financial and monetary calculations.

同样值得注意的是,对于被视为十进制的数字文字,应使用后缀m(表示货币),进一步指出财务数据类型的适当性。


似乎所有这些帖子都很接近,但并不能完全解释问题的症结所在。不是说decimal存储的值更精确,也不是说double存储的数字更多或类似的东西。它们的存储值各不相同。

decimal类型以十进制形式存储值。像1234.567一样。doublefloat以二进制形式存储值,如1101010.0011001。(它们也有存储多少数字的限制,但这在这里并不相关——或者永远都不相关。如果你觉得你的数字不够精确,那你可能是做错了什么)

请注意,有些值不能以任何一种表示法精确存储,因为它们需要在小数点后有无穷多的数字。像1/31/12一样。这样的值在存储时会四舍五入一点,这就是您在这里看到的。

在财务计算中,decimal的优点是可以精确地存储小数部分,而double不能存储小数部分。例如,0.1可以精确地存储在decimal中,但不能存储在double中。这些是货币金额通常采用的价值类型。你不需要存2/3美元,你需要0.66美元。人类货币是基于十进制的,所以decimal类型可以很好地存储它们。

此外,在decimal类型中,增加和减少十进制值也可以完美地工作。这是财务计算中最常见的操作,所以用这种方法编程更容易。

小数值相乘也非常有效,尽管它可以增加用于确保精确值的小数位数。

但是除法是非常危险的,因为通过除法得到的大多数值不能精确存储,并且会出现舍入错误。

一天结束时,doubledecimal都可以用来存储货币价值,您只需要非常小心它们的局限性。对于double类型,您需要在每次计算后对结果取整,甚至加减。每当您向用户显示值时,都需要显式地将其格式化为具有一定数量的十进制数字。此外,在比较数字时,注意只比较前x位小数(通常为2或4)。

对于decimal类型,由于您知道您的货币价值是精确存储的,因此可以放宽其中一些限制。您通常可以跳过加减后的舍入。如果一开始只存储X位十进制数字,则无需担心显式显示格式和比较。它确实使事情变得相当容易。但是你仍然需要在乘法和除法之后进行取整。

这里还有一个更优雅的方法没有讨论。更改货币单位。不要存储美元价值,而是存储美分价值。或者,如果您使用4位十进制数字,请存储1/100分之一。

然后你可以用intlong来处理所有事情!

这与decimal的优点基本相同(精确存储值,加/减精确工作),但需要取整的地方会变得更加明显。然而,一个小缺点是,为显示设置这些值的格式会变得更复杂一些。另一方面,如果你忘了做,那也是显而易见的。这是迄今为止我的首选方法。


十进制存储28-29个有效数字,而双精度存储约15-17个数字

当你把1米除以12米(1米/12米)时,结果是0.0833333333333333333333333333.....3,其中3是无限的。浮动和双循环,直到最近的0.083333333333333329

0.0833333333333333333333333333.....3乘以12000时,结果是999.9999999999999999...999999996,但由于小数点有28-29个有效位,所以它对0.0833333333333333333333333333的估计不超过这个值。当0.0833333333333333333333333333乘以12000时,总的结果是999.9999999999999999999999996

数学上的

1
2
1/12 = 0.0833333333333333333333333333.....3
(1/12) x 12000 = 999.9999999999999999...999999996

数学上的十进制计算

1
2
1m/12m = 0.0833333333333333333333333333
(1m/12m) * 12000 = 999.9999999999999999999999996

数学上的双重评价

1
2
1d/12d = 0.083333333333333329 // looses precision
(1d/12d) * 12000 = 1000 // rounded


所有告诉你使用decimal的人都是正确的。甚至官方文件也说,使用decimal是:

Compared to other floating-point types, the decimal type has more precision and a smaller range, which makes it appropriate for financial and monetary calculations.

你所观察到的看似不正确的行为是因为1/12不能完美地表示为小数。

我稍微修改了您的示例,并将它们作为XUnit测试进行了介绍。示例中的所有断言都将通过。

这就是给你带来麻烦的例子…

1
2
3
4
5
6
7
8
9
10
11
[Fact]
public void FirstApproach()
{
    // First approach:    
    decimal ratio = 1m / 12m;
    decimal amount = 12.000m;

    decimal ratioAmount = amount * ratio;

    Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}

显然,12 * (1/12)应该是1,所以这似乎是错误的。

稍微修改一下,我们就能得到正确的答案…

1
2
3
4
5
6
7
8
9
[Fact]
public void ModifiedFirstApproach()
{
    // Values from first approach,
    // but with intermediate variables removed
    decimal ratioAmount = 12.000m * 1m / 12m;

    Assert.Equal(1.000m, ratioAmount);
}

问题似乎是中间变量ratio,尽管把它看作一个操作顺序问题更准确。括号的添加重新引入了原代码的错误…

1
2
3
4
5
6
7
8
9
[Fact]
public void AnotherModifiedFirstApproach()
{
    // Values from first approach,
    // but with intermediate variables removed
    decimal ratioAmount = 12.000m * (1m / 12m);

    Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}

核心问题可以用一行来说明…

1
2
3
4
5
[Fact]
public void OneTwelfthAsDecimal()
{
    Assert.Equal(0.0833333333333333333333333333m, 1m / 12m);
}

小数1/12只能表示为重复的小数,这使得它不精确。这不是C的错——这只是一个在十进制(以10为基数)数字系统中工作的事实。