最近发现在iOS中将String类型转化为Double类型的时候会有莫名奇妙的精度丢失问题,甚至在Double与Double之间的乘除运算结果也会出现很奇葩的精度问题。试过Objective-C和Swift,都有同样的问题,看来是iOS系统底层对这些情况有特殊处理吧。
下面以一个很常见的场景来说明问题:
假如在你的项目中需要提供给用户“提现”的功能,并收取一定比例的手续费(相信玩过各大比特币交易所的同学深有感触)。要求金额保留两位小数,并以手续费四舍五入为准,计算实际提取金额。
创建很简单的视图:
我们输入提现金额,并实时计算手续费,那么我们可能会这样写:
得到的结果是:
发现问题了吗?
按道理,我们输入1234.1的时候,手续费四舍五入保留两位小数应该是61.71,但这里结果是61.70,而且实际提取的金额和手续费的金额加起来也不等于1234.1。
看图中的断点,是不是很惊讶!为什么inputValue的值是1234.0999999?这就是题目中说的恶心的精度。48行直接将String类型转Double类型就会出现这样奇怪的问题,然后计算的feeValue就相应的变小了一点点,再四舍五入就少了0.01了,同样地,第55行的结果是1172.39499,四舍五入之后变成了1172.39。所以,总的来说,手续费不对,实际提取的也不对了。
怎么办呢? 有两种方法: 一是手动修正手续费的值;二是使用NSDecimalNumber
,也是最推荐的方式。
首先第一种方法,既然我们知道系统会丢失一点精度,那我们在转化之后手动修复一点点,比如给inputValue加上一个很小的小数,然后再对feeValue手动四舍五入,这样就保证了feeValue的值是正确的,也能保证realValue四舍五入的结果是对的:
第二种方法是推荐使用的,在处理金钱相关的精度问题首先应该考虑使用它
其中我们所有的操作都是基于String和NSDecimalNumber的,中间不涉及Double的转换。其中NSDecimalNumberHandler
是可以定义一些行为参数的:
scale : 需要保留的精度。
raiseOnExactness : 为YES时在处理精确时如果有错误,就会抛出异常。
raiseOnOverflow : YES时在计算精度向上溢出时会抛出异常,否则返回。
raiseOnUnderflow : YES时在计算精度向下溢出时会抛出异常,否则返回.
raiseOnDivideByZero : YES时。当除以0时会抛出异常,否则返回。
这样得到的结果是精确的NSDecimalNumber
类型的,我么可以像第56行那样取出他的string格式结果。
使用这两种方式的结果如下: