.NET中的十进制浮点数
在我关于二进制浮点类型的文章(原文 译文)中,我简单地提到了System.Decimal(或者指C#中decimal)类型。本文给出了关于该类型的更多细节,包括它的表示方法以及它与更常见的二进制浮点类型之间的一些区别。本文从这里开始,之后所说的decimal就是指System.Decimal,同理,提到float和double就是指.NET的类型System.Single和System.Double。为了让文章更易阅读,我也会把它们的名字改成正常字体。(注释:译文里还是会区分一下的)
什么是decimal类型?
decimal类型就是另一种浮点数——但不同于float和double,它的基数是10。如果你还没看过上文的文章链接,现在先去看看吧——本文不再提及浮点数的基础知识。
decimal类型和其余浮点数有相同的组成:尾数、指数和符号位。通常来说,decimal类型符号位也只有1位,但是有96位尾数和5位指数。但是,并不是所有的指数组合都有效。只有0-28的数值才有效,而且它们实际上都是负数:数值表示为符号位 * 尾数 / 10指数。这意味着该类型的最大值和最小值是+/-(296-1),最小非零数是10-28。
指数受限的原因是因为尾数位只能存储28或29位小数(取决于具体的数值)。实际上,假如你有28位数字,你就可以设置任何你想要的值,可以将小数点放在第一位数字的左边到最后一位右边的任意位置。(会有一些数字,你可以把第29位数字放在其余数字的左边,但你不可能获取所有29位数字的组合,因此就有了此限制)。
decimal如何存储?
decimal以128位存储,尽管严格来说只有102位是必须的。我们可以简单地把decimal看作是4个32位整型数,其中三个用来表示尾数,其余一个表示符号位和指数。最后一位整数的最高位是符号位(通常来说,负数会设置为1)然后16-23位(高16位字的低位)是指数位。其他位都要清0,。这种表示法是decimal.GetBits(decimal)给出的,它会返回一个含有4个int数的数组。
decimal格式化
与float和double不同,在.NET中,当decimal被转换为字符串时,其默认行为是给出精准的值。这意味着decimal没有类似于二进制浮点数文章提到的DoubleConverter的方法。当然,你可以对其精度进行限制。
保留0位
在.NET 1.0和1.1之间,decimal类型发生了微妙的变化。看看下面这个简单的程序:
1 | using System; |
当我第一次运行上面的程序(或类似的程序)时,我以为它只输出1(这是在.NET 1.0上的结果)——但事实上,输出是1.00。decimal类型不会自己归一化——它会记录自己有多少个小数位(通过尽可能地保存指数),在格式化时,零可能被算作一个重要的小数位。我不知道当两个不同的小数进行相乘、相除、相加等一系列操作时,是如何选择指数的(在有选择的情况下),你可以跑跑下面的程序,会发现很有意思:
1 | using System; |
结果如下:
1 | 0.00000000000010000 |
一切都是数字
decimal类型infinity和NaN(not a number)的概念,尽管上面的例子说明同一个数字会有不同的表现形式(例如1,1.0,1.00),但通常==运算符就可以处理这些问题,例如1.0==1.00会返回True。
精准度
虽然decimal类型的潜在指数范围比较小,但它的精度比任何.NET中的内置二进制浮点数都要高。而且,在二进制浮点数中因为原始操作数精度不够而造成的奇怪结果,在decimal中并不会存在,这是因为许多操作数在源码中就是表示为十进制(decimals)的。然而,这并不意味着所有的操作都会变得精准:例如,三分之一仍然无法准确表示。其潜在的问题和二进制浮点的问题是一样的。但是,大多数时候,类似货币这样的数值,仍然要选择使用decimal类型,这会使操作变得简单,还能保持精度(例如,添加一个指定为百分比的税金,仍可以保持数字的准确性,前提是这些数字在合理的范围内)。只需要注意哪些操作会导致不精确,哪些不会。
有一个十分宽泛的经验,如果你的结果用字符串表示后非常长(即28/29位的大部分数字都是非零的)那么就有可能在过程中出现了有误差的情况:如果数字精确,那么使用decimal类型表示的数字不会有那么多位有效数字。
总结
大多数商业应用应当使用decimal而不是float或double。我的经验法则是,货币等人造值通常用decimal来表示比较好:例如,正好是1.15美元的概念是完全合理的。对于来自自然界的值,如长度和重量,二进制浮点类型更有意义。即使理论上有 “正好是1.15米”,但在现实中也不会出现:你肯定永远无法测量出精准的长度,在原子水平上它们甚至都不太可能存在。我们习惯于其中存在一些误差。
使用decimal浮点运算是需要付出代价的,但我认为这不太可能成为大多数开发者的瓶颈。一如既往,先写出最合适的(可读的)代码,然后再继续分析你的性能。通常情况下,缓慢得到正确的答案总比快速得到错误的答案要好得多——尤其是在涉及到钱的时候…
本文是对Decimal floating point in .NET这篇文章的翻译,作者是著有C# in Depth的大神Jon Skeet。非文章原文或本人对某段文字理解,会以斜体 个人理解:xxx 进行标注。本人翻译能力有限,强烈建议大家去看原版!