qinghuan
← 回到知识探索

计算机趣闻 / 趣味知识

0.1 加 0.2,为什么不是刚好 0.3?

先分清存进去的近似值、运算后的值和屏幕上的文字,再决定如何比较小数。

展开本文目录
  1. 先动手看看
  2. 有些有限小数,换成二进制就写不完
  3. 输入、计算和显示,是三件事
  4. 显示两位小数,不等于修好了内部数据
  5. 比较之前,先说明“足够接近”是什么意思
  6. 记住这三点
  7. 别把例子用过头
  8. 自己试一小步
  9. 查原始资料
  10. 找一份资料继续练
开始前,会这些就够了
  • 理解小数、分数和简单加法
遇到不熟悉的词,先看这 1 个解释
浮点数
浮点数用有限的有效数字配合指数表示数值,像一种可移动小数点的记法。它能覆盖很大的数值范围,却只能保留有限精度,许多十进制小数存进去时需要取近似。
打开完整术语小抄 ↗

在 JavaScript 中计算 0.1 + 0.2,常会看到 0.30000000000000004。多出来的尾巴很像一次算错,但计算机没有偷偷把十进制加法表改掉;问题从这两个数存进有限空间时就开始了。

想象你只能写六位小数。三分之一只能暂记成 0.333333,再乘三也未必回到精确的一。换一种进位方式,相同的困难就会落到我们非常熟悉的十分之一身上。

让小数自己说明问题

先预测严格相等的结果,再换成 0.25 + 0.5 与 0.75 比较。

相加结果:0.30000000000000004

严格相等:否

两者之差的绝对值:5.551115123125783e-17

相加结果的 8 个字节(大端展示):3f d3 33 33 33 33 33 34

比较数的 8 个字节:3f d3 33 33 33 33 33 33

使用浏览器的 JavaScript Number(二进制 64 位浮点数)。显示更多位数不会提高存储精度。下面的误差是两种浮点计算结果之间的差,不是任意精度十进制证明。

有些有限小数,换成二进制就写不完

十进制小数用十分之一、百分之一等份额拼数;二进制小数改用二分之一、四分之一、八分之一。0.5 和 0.25 刚好能用有限个这样的份额表示,0.1 却需要不断延续。普通浮点数只保留有限有效位,只能选一个邻近的可表示值。

JavaScript 的 Number 使用双精度二进制浮点数。可以把可表示值想成数轴上一组间隔并不始终相同的刻度:运算结果落在刻度之间时,要舍入到允许的位置。数字变大后,附近刻度通常也更疏,因此不能把所有数都想成精确到固定小数位。

输入、计算和显示,是三件事

写下 0.1 时,程序先保存它的二进制近似值;加法对保存的值进行运算,并可能再次舍入。屏幕显示还要把结果转成十进制文字,通常选择适合阅读、能区分原值的写法。平时打印 0.1 很短,不代表内部保存的是精确的十分之一。

所以尾巴有时出现、有时没有。下面的 0.5 加 0.25 可以精确表示,并不是所有小数运算都会产生肉眼可见的误差。不要把现象概括成“计算机不会算小数”,更有用的问题是:这个任务允许多大误差,数据又是怎么表示的?

小实验 · javascript
const total = 0.1 + 0.2;
console.log(total);
console.log(total === 0.3);
console.log(total.toFixed(2));
console.log(typeof total.toFixed(2));
console.log(0.5 + 0.25 === 0.75);

怎样运行:保存为 decimal-demo.js,运行 node decimal-demo.js;不需要第三方依赖。

运行后对照结果
0.30000000000000004
false
0.30
string
true

显示两位小数,不等于修好了内部数据

toFixed(2) 得到的是用于显示的字符串。它让用户看到 0.30,但没有改变变量 total。把这个字符串再转成 Number,也只是再次选择一个可表示值。如果后面还要分摊、累计或计算比例,应先决定整个计算过程的精度规则。

例如一种金额始终以分为最小单位,输入也已经被严格转换成整数分,那么 10 分加 20 分可以直接做整数加法。这里的前提很重要:税率、分摊余数、超大金额和输入转换仍需约定;不能把任意小数乘一百再四舍五入当作通用方案。

比较之前,先说明“足够接近”是什么意思

测量值和数值算法常需要容差,也就是允许差多少。Number.EPSILON 描述的是一附近相邻数的间隔,不是每个场景通用的及格线。比较一米范围的误差和天文距离上的误差,不能机械使用同一个绝对阈值;输入本身的测量精度也会限制结论。

需要精确十进制规则时,可以选合适的十进制类型或整数单位,并明确舍入时机;需要科学计算时,应考虑算法的误差传播。先写一个能说明业务目的的例子,再选表示和比较方法,比到处把等号替换成“差小于某个极小数”更可靠。

记住这三点

  • 0.1 的有限二进制表示只能近似;加法和显示还会分别舍入。
  • 格式化负责显示,不会自动改变已有变量或后续计算规则。
  • 容差、整数单位和十进制类型要按任务选择,EPSILON 不是万能阈值。

别把例子用过头

  • 不能从一个带尾巴的结果推断所有小数都不精确。
  • 金额使用整数单位也要处理输入、范围和舍入,不是简单乘一百就完成设计。

自己试一小步

比较两组尺度不同的数,说明为什么同一个容差可能不适用。

  1. 先尝试小数加法,再把每个输入扩大一万倍,记录结果与差值。
  2. 分别规定一个绝对误差要求和一个相对误差要求,再检查两组结果。
  3. 为“显示两位小数”和“内部继续累计”分别写出处理方案。

做到这些,就算完成

  • 没有只根据屏幕文字判断两个内部数完全相等。
  • 容差由题目需要解释,而不是随手写一个极小常数。
  • 方案说明采用的单位、精度与舍入时机,不提供未经检查的金额函数。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。