这个问题在这里已有答案:
我们正在进行财务计算。我发现这篇关于将货币值存储为小数的帖子:decimal vs double! - Which one should I use and when?
所以我将金额存储为小数。
我有以下计算:12.000 *(1/12)= 1.000
如果我使用十进制数据类型来存储金额和结果金额,我得不到预期的结果
// 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
什么是最好的方法?每个人都在谈论金额/金钱必须存储为小数。
永远不会永远存储金融金额。这是from my blog的一个例子,它显示了为什么不应该使用double
:
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!");
}
你会看到Console.WriteLine
会被击中,因为双打实际上加起来是31995.270000000004。正如您可以从变量名称中猜测的那样,此代码示例基于财务系统中的一些实际代码 - 此问题导致用户无法正确地为交易分配金额。
使用以下附加代码将数字添加为decimal
s:
decimal decAvailable = (decimal)dblAvailable;
decimal decTotal = (decimal)dblTotal;
if (decAvailable < decTotal)
{
Console.WriteLine("They still don't add up!");
}
不会打到Console.WriteLine
。故事的寓意:使用decimal
进行财务计算!
language reference for the decimal keyword的第一部分指出:
与其他浮点类型相比,
decimal
类型具有更高的精度和更小的范围,这使其适用于财务和货币计算。
还值得注意的是,对于要被视为小数的数字文字,应使用后缀m
(用于货币),进一步指向财务数据类型的适当性。
似乎所有这些帖子都接近了,但并没有完全解释问题的关键。这不是decimal
更准确地存储价值或double
有更多的数字或类似的东西。它们各自存储的值不同。
decimal
类型以十进制形式存储值。像1234.567
。 double
(和float
)以二进制形式存储值,如1101010.0011001
。 (他们也有限制他们可以存储多少位数,但这在这里并不相关 - 或者永远不会。如果你觉得你的精确度已经没有数字了,你可能做错了什么)
请注意,某些值无法以两种符号精确存储,因为它们在小数点后需要无限量的数字。像1/3
或1/12
。这些值在存储时会略微调整,这就是您在这里看到的。
decimal
在财务计算中的优势在于它可以精确地存储小数部分,而double
则不能。例如,0.1
可以精确地存储在decimal
中,但不能存储在double
中。这些是金额通常采用的价值。你永远不需要存储2/3美元,你需要0.66美元。人类货币是基于小数的,因此decimal
类型可以很好地存储它们。
此外,加上和减去十进制值也可以与decimal
类型完美配合。这是财务计算中最常见的操作,因此以这种方式编程更容易。
乘以十进制值的效果也很好,但它可以增加用于确保精确值的小数位数。
但是划分是非常危险的,因为通过划分获得的大多数值将不能精确存储并且将发生舍入误差。
在一天结束时,double
和decimal
都可以用来存储货币价值,你只需要非常小心它们的局限性。对于double
类型,您需要在每次计算后舍入结果,甚至加法和减法。无论何时向用户显示值,都需要将它们显式格式化为具有一定数量的十进制数字。此外,在比较数字时,请注意只比较前X个十进制数字(通常为2或4)。
对于decimal
类型,可以放宽这些限制中的一些,因为您知道您的货币价值是精确存储的。加法和减法后,您通常可以跳过舍入。如果您只在第一个位置存储X十进制数字,则无需担心显式显示格式和比较。它确实使事情变得相当容易。但是你需要在乘法和除法之后进行舍入。
这里没有讨论一种更优雅的方法。改变你的货币单位。而不是存储美元价值,存储分值。或者,如果您使用4位十进制数字,则存储1/100分。
然后你可以使用int
或long
的一切!
这具有decimal
的大部分相同优点(精确存储的值,加法/减法精确地工作),但是你需要对事物进行舍入的地方将变得更加明显。然而,一个小缺点是格式化这样的显示值变得有点复杂。另一方面,如果你忘记这样做,那也是显而易见的。到目前为止,这是我的首选方法。
十进制存储28-29有效数字,而双存储~15-17位数
当你将1m划分为12m(1m / 12m)时,其结果是0.0833333333333333333333333333.....3
,其中3s是无限的。浮动和双倍舍入到最近的0.083333333333333329
。
当0.0833333333333333333333333333.....3
乘以12000时,结果是999.9999999999999999...999999996
,但由于Decimal有28-29个重要的挖掘位置,它不会比这更好地评估0.0833333333333333333333333333
。当0.0833333333333333333333333333
乘以12000时,总体结果是999.9999999999999999999999996
数学
1/12 = 0.0833333333333333333333333333.....3
(1/12) x 12000 = 999.9999999999999999...999999996
数学上Decimal评估
1m/12m = 0.0833333333333333333333333333
(1m/12m) * 12000 = 999.9999999999999999999999996
数学双重评估
1d/12d = 0.083333333333333329 // looses precision
(1d/12d) * 12000 = 1000 // rounded
每个人都告诉你使用decimal
是正确的。甚至the official docs说decimal
是使用的东西:
与其他浮点类型相比,十进制类型具有更高的精度和更小的范围,这使其适用于财务和货币计算。
您观察到的看似不正确的行为来自于1/12不能完美地表示为小数的事实。
我稍微修改了你的例子,并将它们作为xUnit测试呈现。示例中的所有断言都通过了。
这是给你麻烦的例子......
[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
,所以这似乎是错误的。
经过略微修改,我们可以得到“正确”的答案......
[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
,尽管将其视为一个操作顺序问题更为准确。括号的添加重新引入了原始代码中的错误......
[Fact]
public void AnotherModifiedFirstApproach()
{
// Values from first approach,
// but with intermediate variables removed
decimal ratioAmount = 12.000m * (1m / 12m);
Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}
核心问题可以用一行来说明......
[Fact]
public void OneTwelfthAsDecimal()
{
Assert.Equal(0.0833333333333333333333333333m, 1m / 12m);
}
分数1/12
只能表示为重复小数,这使得它不精确。这不是C#的错 - 它只是在十进制(基数为10)数字系统中工作的事实。