Swift 中如何避免精度丟失

若是你開發過涉及金額計算的 iOS app, 那麼你頗有可能經歷過在使用浮點型數字時精度丟失的問題編程

himg

讓咱們來看看爲何會丟失以及如何解決吧json

浮點型數字的數值精度爲什麼會丟失?

這裏我不想系統地講解浮點型是如何由基數尾數指數組成的, 直接說緣由: 由於用二進制能表示的以 2 爲底的指數必然是 2 的倍數, 也就是說只能爲 0.5, 0.25, 0.125... 以此類推, 那麼咱們就能夠發現不管將這些數字怎麼組合, 都不可能達到 0.3 這個值, 所以計算機這個時候會給咱們一個最接近 0.3 且剛好是這些數字之和的一個近似值.swift

himg

所以, 對於精度丟失咱們能夠得出以下結論:數組

  • 在 Swift 裏面整數是不會有精度丟失的問題的, 由於整數的跨度爲 1, 1 是能夠被 2 進製表示出來的
  • 因爲 Swift 編程語言存儲浮點型的方式問題, 浮點型 (Double/Float) 的精度丟失問題是必然會發生的

數值精度丟失的影響

上面咱們簡單的解釋了爲何會丟失精度, 那麼精度丟失對咱們在何時有影響呢?服務器

根據個人經驗, 我認爲主要場景集中以下:markdown

  • 在須要將數字以字面值向外界展現的時候
  • 在須要將數字發向服務器進行嚴格對比 (每一位都不能有差異)

因此, 精度丟失並不可怕 (起碼出現的場景不多). 下面讓咱們看下如何才能在咱們真的遇到了精度丟失問題時候進行解決app

如何應對數值精度丟失

  1. 計算過程當中全程使用 Double, 最後轉爲字符串編程語言

    因爲 Swift 在精度丟失時會在保留不少位小數 (好比 0.3 存儲爲 0.29999999999999999), 這些小數與真實值的差距很是之小, 所以咱們徹底能夠在過程當中不對其進行任何操做, 仍然讓其保持 Double 類型, 在最後時刻要發往服務器或者顯示的時候咱們將其四捨五入轉換爲字符串, 這樣的結果基本不會出錯.ide

    可是切記必定不要在計算過程當中進行四捨五入, 不然極有可能會形成偏差的累計, 從而致使偏差變大不可接受.oop

  2. Decimal 格式進行接收並計算

    上面的方式簡單, 只須要注意在最後時刻進行一次字符串轉換便可, 可是有缺陷: 必須讓服務器將本來的數字類型轉爲以字符串類型來接收, 這並非一種友好的方式. 那麼咱們到底有沒有辦法讓 app 向服務器發送一個帶有精度不丟失的浮點數字的 json 數據包呢? 好比 {"num": 0.3}, 而不是 {"num": 0.29999999999999999}

    答案是能夠. Swift 爲咱們提供了用於十進制計算的一個類型: Decimal, 這個類型也帶有 +, -, *, / 運算符, 而且支持 Codable 協議, 咱們徹底能夠定義此類型接受服務器的參數值, 而後以此類型進行運算而後使用, 最後, 由於其支持 Codable 協議, 咱們能夠將其值直接放入 json 包中. 沒有特殊狀況的話咱們就徹底避開了二進制浮點型數字了, 這樣是不會有任何的偏差的

    himg

    himg

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)
複製代碼

himg

答案是簡單的: 咱們使用的 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"]
    }
}
複製代碼

himg

正確使用 Decimal 的初始化方式

Decimal 有多種初始化方式, 咱們能夠傳入整型值, 傳入浮點型, 傳入字符串方式進行初始化, 我認爲正確的初始化方式應該是使用字符串.

himg

上面這張圖應該很簡單明瞭的說明了我爲何這麼認爲了. 其緣由與上個反序列問題類似, 也是由於咱們傳入 Double 時, Swift 對其進行了一次承載, 這一次承載就對其形成了精度丟失, 根據已經丟失精度的 Double 初始化出 Decimal, 這個 Decimal 是精度丟失的也就不難理解了

參考

相關文章
相關標籤/搜索