编程与类型系统
上QQ阅读APP看书,第一时间看更新

2.3.2 浮点类型和圆整

IEEE 754是美国电气和电子工程师协会(Institute of Electrical and Electronics Engineers)为表示浮点数(带小数部分的数字)制定的标准。在TypeScript(和JavaScript)中,使用binary64编码将数字表示为64位浮点数。图2.5详细说明了这种表示。

图2.5 0.10的浮点数表示。最上面显示了浮点数的三个部分(符号位、指数和尾数)在内存中的二进制表示。中间显示了将二进制表示转换为数字的公式。最下面显示了应用公式的结果:0.1被近似为0.100000000000000005551115123126

浮点数的3个部分包括符号、指数和尾数。符号是一个位,对于正数为0,对于负数为1。尾数是用图2.2中的公式描述的小数部分。这个小数部分将会与2的偏移指数次方相乘。

称之为指数偏移,是因为我们从指数表示的无符号整数中减去一个值,从而能够表示正数和负数。在binary64编码中,这个值是1023。IEEE 754标准定义了几种编码,一些以10而不是2为基,不过在实际应用中,以2为基出现得更多。

标准还定义了一些特殊值:

NaN,代表非数字(not a number),用于表示无效操作(如除零)的结果。

▪ 正无穷和负无穷(Inf),当操作溢出时用作饱和值。

▪ 尽管根据公式,0.10变成了0.100000000000000005551115123126,但这个数字将被向下圆整到0.1。事实上,在JavaScript中进行比较时,认为0.10和0.100000000000000005551115123126相等。浮点数只能通过圆整和近似,才能使用相对少量的位数,表示很大范围中的小数。

精度值

如果需要处理精度(例如在处理货币时),则避免使用浮点数。将0.10相加3次,得到的结果并不等于0.30,这是因为虽然每个0.10的表示被圆整为0.10,但把它们相加3次后,圆整后的结果将是0.30000000000000004。

不需要圆整,就能够安全地表示小整数,所以更好的方法是将一个价格编码为由美元整数和美分整数组成的一对值。JavaScript提供了Number.isSafeInteger(),可用来了解一个整数值是否能够在不被圆整的情况下表示出来,所以依赖于该函数的结果,我们可以设计一个Currency类型,使其编码两个整数值,并且防范圆整问题,如程序清单2.9所示。

程序清单2.9 Currency类和货币加法函数

在另外一种语言中,我们会使用两个整数类型,并防范上溢/下溢。因为JavaScript没有提供整数基本类型,所以我们需要依赖于Number.isSafeInteger()来防范圆整。在处理货币时,相比让资金神秘地出现或者消失,报错是更好的处理方式。

程序清单2.9中的类仍然非常基础。来看一个很有帮助的练习:可以将每100美分自动转换为1美元。此时,应该考虑在什么位置检查整数的安全性。如果美元值是一个安全的整数,但将其加1后(这个1来自100美分),它变得不再安全,此时应该怎么办?

比较浮点数

我们看到,由于存在圆整,比较浮点数的相等性通常不是一个好主意。有一种更好的方法来知道两个值是否近似相等:我们可以确保它们的差在给定阈值内。

这个阈值应该是多大?它应该是可能出现的最大圆整误差。这个值叫作machine epsilon,随不同的编码而可能有所变化。JavaScript将这个值作为Number.EPSILON提供。使用这个值时,我们可以实现两个数字的相等性比较,只需取出两个数字的差的绝对值,然后检查它是否小于machine epsilon即可。如果小于,则两个值在彼此的圆整误差之内,我们可以认为它们相等,如程序清单2.10所示。

程序清单2.10 使用machine epsilon判断浮点数是否相等

一般来说,在比较两个浮点数时,使用类似epsilonEqual()这样的函数是个好主意,因为算术运算可能导致圆整误差,进而导致意外的结果。