.NET 中的十进制浮点类型(译文)

在我的二进制浮点类型文章里,我简要提到了System.Decimal类型(或者仅仅是C#中的十进制类型)。这篇文章给出了更多关于这个类型的细节,包括它的表示和它与更多公共二进制浮点类型的区别。从这开始,我应该仅仅称它为十进制类型而不是System.Decimal, 这同样是单精度浮点类型(float)和双精度浮点类型(double)所提及的, 我的意思分别是指.NET 类型中的System.Single和System.Double. 为了让这篇文章看起来很容易,我也将从这开始弃用正式类型中的名字。

什么是十进制类型?

十进制类型仅是浮点数的另外一种表示形式 - 但是与单精度浮点类型和双精度浮点类型不同,十进制类型使用的进制是10。如果你还没有读上面链接给出的文章,现在将会是读它的最佳时间 - 我将不会在这篇文章中探讨浮点类型数字。

十进制类型于任何其他浮点数字有同样的组件: 一个尾数一个指数一个符号。按照惯例, 符号位仅是一个比特,但是有96比特的尾数和5比特的指数位。然而,并不是所有指数组合都是合法的。只有值是0~28的才能工作,所有的负数也都是有效的: 数字值是符号*尾数/10指数。这意味着这个类型的最大值和最小值是+/- (296-1), 最小的非零数字在绝对值上是10-28.

指数被限制的原因是尾数可以存储28或29个十进制数字(取决于它自己的精度)。你可以将28个数字可以设置成任何你想要的值,你可以将十进制浮点放在第一个数字的左边到最后一个数字的右边间的任何一个地方,(有一些你可以让第29个数字在其余数字左边的数字,但是由于限制,你不可以使用29个数字的所有组合。)它们都是有效的。

一个十进制数是如何存储的?

一个十进制数用128比特存储,尽管只有102比特是严格必须要有的。把十进制数认为是由尾数表示的3个32位整型数是很方便的,然后就可以用一个整数表示符号位和指数位。最后一个整数的最高位是符号位(在正式方式中,将最高位设置成(1)表示负数)同时16~23位(高16位字的低位)表示指数。其他位必须都是(0). 这个表示是由decimal.GetBits(decimal)提供的可以返回一个4个整型数数组的方式。

格式化十进制数

与单精度浮点数和双精度浮点数不同,当.NET被要求将一个十进制数格式化成一个字符串表示形式时,它的默认行为是给出精确值。这意味着二进制浮点类型文章之中的DoubleConverter代码 里提到的一个十进制等效是没有必要的。当然,你可以用它来将值限制到一个特殊的精度。

保留0

在.NET 1.0 和1.1 间, 十进制类型经历了一个微妙的变化。考虑下面的简单代码:

using System;

public class Test
{
    static void Main()
    {
        decimal d = 1.00m;
        Console.WriteLine (d);
    }
}
当我首先运行上面的代码时(或者一些类似的),我期待它输出的结果是1(这是.NET 1.0中应该有的结果) - 但实际上,输出是1.00。十进制类型没有使用它自己的标准 - 它记住了它有多少个十进制数字(通过维护可能的说明)并格式化,0可能被计入一个重要的十进制数字。当两个不同的十进制数相乘,相除,相加等等时,我不知道选择哪个说明(这里有一个选择)的精确规则,但是当你用如下的程序测试时你可能会发现它很有趣。
using System;

public class Test
{
    static void Main()
    {
        decimal d = 0.000 000 000 00010000m;
        while (d != 0m)
        {
            Console.WriteLine (d);
            d = d/5m;
        }
    }
}

它生成了一系列结果:

0.00000000000010000
0.00000000000002000
0.00000000000000400
0.00000000000000080
0.00000000000000016
0.000000000000000032
0.0000000000000000064
0.00000000000000000128
0.000000000000000000256
0.0000000000000000000512
0.00000000000000000001024
0.000000000000000000002048
0.0000000000000000000004096
0.00000000000000000000008192
0.000000000000000000000016384
0.0000000000000000000000032768
0.0000000000000000000000006554
0.0000000000000000000000001311
0.0000000000000000000000000262
0.0000000000000000000000000052
0.000000000000000000000000001
0.0000000000000000000000000002

所有的都是一个数字

十进制类型没有无穷大或者NaN(not-a-number, 不是一个数字)值,虽然上面例子里的同样的真实数字隐含是不同格式(比如1, 1.0, 1.00),正常的==操作符考虑了这些并报告1.0==1.00.

精确性

在.NET中十进制类型比任何内建的浮点类型有更大的精度,尽管它有一个相对比较小的默认指数范围。很多操作使用二进制浮点类型而没有使用十进制浮点类型来不精确的表示原始操作数却获得了让人感觉惊讶的结果,精度是因为很多操作在源码中按照十进制表示的。然而,那并不意味着所有操作突然变得精确起来: 1/3仍然是不完全表示的,例如,潜在的问题与二进制浮点的问题一样。然而,大多数时候十进制类型用来计算总量,比如钱,操作很简单同时保持结果精确。(例如,添加一个由百分比表示的税负将会让数字精确,假设它们要在一个可以判断的范围内开始。)仅需要注意哪些操作可能引起不精确,哪些操作不会。

作为一个非常广泛的拇指(译注: thumb 翻译为拇指可能不准确,以下类同)规则,如果你结束查看一个非常长的字符串表示行为(比如大多数28/29位数字都是非零的)那么有可能你会在这个过程中得到一些不精确: 大多数对十进制类型的使用不会再数字已经精确的情况下仍然使用很多重要的数字来结束。

结论

大多数商业应用应该可能使用十进制而不是单精度浮点或者双精度浮点。我的拇指规则是由人类制作的值比如货币通常用十进制浮点类型表示比较好: 例如,‘精确的1.25美元’这个的概念完全是有理由的。对来自自然世界的值,比如长度和重量,二进制浮点类型会更加有意义。尽管有一个理论的”精确1.25米”但它不会真的在现实世界中发生: 你当然不会去精确的测量长度,而且它们也不像是能在原子层次中存在。我们通常有一个可以忍耐的范围。

使用十进制浮点算法需要花费一个代价,但是我认为这对大多数开发人员来说不是瓶颈。总之,首先写最合适(可读)的代码,一直分析你的代码性能。通常慢一点获得正确答案比很快却得到错误的答案更好 - 尤其是对钱来说…

原文请参考 http://csharpindepth.com/Articles/General/Decimal.aspx 


作者:DanielWise
出处:http://www.cnblogs.com/danielWise/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/danielWise/p/1915191.html