Swift 精度问题2

1,015 阅读3分钟

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

NSDecimalNumber 与 Decimal 区别

NSDecimalNumberNSNumber 的一个子类, 比 NSNumber 的功能更为强大, 四舍五入, 取整, 输入后自动去掉数值前面无用的 0 等等. 由于 NSDecimalNumber 精度较高, 所以会比基本数据类型费时, 所以需要权衡考虑, 苹果官方建议在货币以及要求精度很高的场景下使用.

通常情况下我们会使用 NSDecimalNumberHandler 这个格式化器对其需要约束的格式进行设置, 然后构建出需要的 NSDecimalNumber

let ouncesDecimal: NSDecimalNumber = NSDecimalNumber(value: doubleValue)
let behavior: NSDecimalNumberHandler = NSDecimalNumberHandler(roundingMode: mode,
                                                              scale: Int16(decimal),
                                                              raiseOnExactness: false,
                                                              raiseOnOverflow: false,
                                                              raiseOnUnderflow: false,
                                                              raiseOnDivideByZero: false)
let roundedOunces: NSDecimalNumber = ouncesDecimal.rounding(accordingToBehavior: behavior)

NSDecimalNumberDecimal 基本是无缝桥接的, Decimal 是一个值类型 Struct, NSDecimalNumber 是一个引用类型 Class, 看起来 NSDecimalNumber 的设置功能更为丰富, 但是如果只是需要对位数, 四舍五入方式有要求的话 Decimal 也完全可以满足, 而且性能会更好, 所以我认为 NSDecimalNumber 仅在 Decimal 无法实现某个功能时才作为备用考虑.

总的来说, NSDecimalNumberDecimal 的关系类似 NSStringString 的关系.

Decimal 的正确使用方式

正确使用 json 反序列化对 Decimal 进行赋值 -- 使用 ObjectMapper

当我们声明一个 Decimal 属性后, 然后使用一个 json 字符串对其进行赋值, 我们会发现精度仍然丢失了, 为什么会有这样的结果呢?

struct Money: Codable {
    let amount: Decimal
    let currency: String
}

let json = "{"amount": 9021.234891,"currency": "CNY"}"
let jsonData = json.data(using: .utf8)!
let decoder = JSONDecoder()

let money = try! decoder.decode(Money.self, from: jsonData)
print(money.amount)

image.png

答案是简单的: 我们使用的 JSONDecoder() 内部使用了 JSONSerialization() 进行反序列化, 其逻辑非常简单, 在碰到 9021.234891 这个数字时, 其会毫不犹豫的将其看做 Double 类型, 然后再将 Double 转为 Decimal 是可以成功的, 但是这个时候已经是精度丢失的 Double 了, 转换得来的 Decimal 类型自然也是精度丢失的.

对于这个问题, 我们必须要能够控制其反序列化过程. 我现在的选择方案是使用 ObjectMapper, 其可以使用自定义规则灵活控制序列化与反序列化的过程.

ObjectMapper 默认情况下是不支持 Decimal 的, 我们可以自定义一个支持 Decimal 类型的 TransformType, 如下:

open class DecimalTransform: TransformType {
    public typealias Object = Decimal
    public typealias JSON = Decimal

    public init() {}

    open func transformFromJSON(_ value: Any?) -> Decimal? {
        if let number = value as? NSNumber {
            return Decimal(string: number.description)
        } else if let string = value as? String {
            return Decimal(string: string)
        }
        return nil
    }

    open func transformToJSON(_ value: Decimal?) -> Decimal? {
        return value
    }
}

然后将此 TransformType 应用于我们需要转换的属性上

struct Money: Mappable {
    var amount: Decimal?
    var currency: String?

    init() { }
    init?(map: Map) { }

    mutating func mapping(map: Map) {
        amount <- (map["amount"], DecimalTransform())
        currency <- map["currency"]
    }
}

image.png

正确使用 Decimal 的初始化方式

Decimal 有多种初始化方式, 我们可以传入整型值, 传入浮点型, 传入字符串方式进行初始化, 我认为正确的初始化方式应该是使用字符串.

image.png

上面这张图应该很简单明了的说明了我为什么这么认为了. 其原因与上个反序列问题相似, 也是因为我们传入 Double 时, Swift 对其进行了一次承载, 这一次承载就对其造成了精度丢失, 根据已经丢失精度的 Double 初始化出 Decimal, 这个 Decimal 是精度丢失的也就不难理解了

参考

Decoding money from JSON in Swift