Swift 精度问题

1,026 阅读3分钟

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。

如果你开发过涉及金额计算的 iOS app, 那么你很有可能经历过在使用浮点型数字时精度丢失的问题

image.png

让我们来看看为什么会丢失以及如何解决吧

浮点型数字的数值精度为何会丢失?

这里我不想系统地讲解浮点型是如何由基数尾数指数组成的, 直接说原因: 因为用二进制能表示的以 2 为底的指数必然是 2 的倍数, 也就是说只能为 0.5, 0.25, 0.125... 以此类推, 那么我们就可以发现无论将这些数字怎么组合, 都不可能达到 0.3 这个值, 因此计算机这个时候会给我们一个最接近 0.3 且恰好是这些数字之和的一个近似值.

image.png

因此, 对于精度丢失我们可以得出如下结论:

  • 在 Swift 里面整数是不会有精度丢失的问题的, 因为整数的跨度为 1, 1 是可以被 2 进制表示出来的
  • 由于 Swift 编程语言存储浮点型的方式问题, 浮点型 (Double/Float) 的精度丢失问题是必然会发生的

数值精度丢失的影响

上面我们简单的解释了为什么会丢失精度, 那么精度丢失对我们在什么时候有影响呢?

根据我的经验, 我认为主要场景集中如下:

  • 在需要将数字以字面值向外界展示的时候
  • 在需要将数字发向服务器进行严格对比 (每一位都不能有差别)

所以, 精度丢失并不可怕 (起码出现的场景很少). 下面让我们看下如何才能在我们真的遇到了精度丢失问题时候进行解决

如何应对数值精度丢失

  1. 计算过程中全程使用 Double, 最后转为字符串

    由于 Swift 在精度丢失时会在保留很多位小数 (比如 0.3 存储为 0.29999999999999999), 这些小数与真实值的差距非常之小, 因此我们完全可以在过程中不对其进行任何操作, 仍然让其保持 Double 类型, 在最后时刻要发往服务器或者显示的时候我们将其四舍五入转换为字符串, 这样的结果基本不会出错.

    但是切记一定不要在计算过程中进行四舍五入, 否则极有可能会造成误差的累计, 从而导致误差变大不可接受.

  2. Decimal 格式进行接收并计算

    上面的方式简单, 只需要注意在最后时刻进行一次字符串转换即可, 但是有缺陷: 必须让服务器将原本的数字类型转为以字符串类型来接收, 这并不是一种友好的方式. 那么我们到底有没有办法让 app 向服务器发送一个带有精度不丢失的浮点数字的 json 数据包呢? 比如 {"num": 0.3}, 而不是 {"num": 0.29999999999999999}

    答案是可以. Swift 为我们提供了用于十进制计算的一个类型: Decimal, 这个类型也带有 +, -, *, / 运算符, 并且支持 Codable 协议, 我们完全可以定义此类型接受服务器的参数值, 然后以此类型进行运算然后使用, 最后, 因为其支持 Codable 协议, 我们可以将其值直接放入 json 包中. 没有特殊情况的话我们就完全避开了二进制浮点型数字了, 这样是不会有任何的误差的

image.png

image.png