若是你開發過涉及金額計算的 iOS app, 那麼你頗有可能經歷過在使用浮點型數字時精度丟失的問題編程
讓咱們來看看爲何會丟失以及如何解決吧json
這裏我不想系統地講解浮點型是如何由基數尾數指數組成的, 直接說緣由: 由於用二進制能表示的以 2 爲底的指數必然是 2 的倍數, 也就是說只能爲 0.5
, 0.25
, 0.125
... 以此類推, 那麼咱們就能夠發現不管將這些數字怎麼組合, 都不可能達到 0.3
這個值, 所以計算機這個時候會給咱們一個最接近 0.3
且剛好是這些數字之和的一個近似值.swift
所以, 對於精度丟失咱們能夠得出以下結論:數組
Double
/Float
) 的精度丟失問題是必然會發生的上面咱們簡單的解釋了爲何會丟失精度, 那麼精度丟失對咱們在何時有影響呢?服務器
根據個人經驗, 我認爲主要場景集中以下:markdown
因此, 精度丟失並不可怕 (起碼出現的場景不多). 下面讓咱們看下如何才能在咱們真的遇到了精度丟失問題時候進行解決app
計算過程當中全程使用 Double
, 最後轉爲字符串編程語言
因爲 Swift 在精度丟失時會在保留不少位小數 (好比 0.3
存儲爲 0.29999999999999999
), 這些小數與真實值的差距很是之小, 所以咱們徹底能夠在過程當中不對其進行任何操做, 仍然讓其保持 Double
類型, 在最後時刻要發往服務器或者顯示的時候咱們將其四捨五入轉換爲字符串, 這樣的結果基本不會出錯.ide
可是切記必定不要在計算過程當中進行四捨五入, 不然極有可能會形成偏差的累計, 從而致使偏差變大不可接受.oop
以 Decimal
格式進行接收並計算
上面的方式簡單, 只須要注意在最後時刻進行一次字符串轉換便可, 可是有缺陷: 必須讓服務器將本來的數字類型轉爲以字符串類型來接收, 這並非一種友好的方式. 那麼咱們到底有沒有辦法讓 app 向服務器發送一個帶有精度不丟失的浮點數字的 json
數據包呢? 好比 {"num": 0.3}
, 而不是 {"num": 0.29999999999999999}
答案是能夠. Swift 爲咱們提供了用於十進制計算的一個類型: Decimal
, 這個類型也帶有 +
, -
, *
, /
運算符, 而且支持 Codable
協議, 咱們徹底能夠定義此類型接受服務器的參數值, 而後以此類型進行運算而後使用, 最後, 由於其支持 Codable
協議, 咱們能夠將其值直接放入 json 包中. 沒有特殊狀況的話咱們就徹底避開了二進制浮點型數字了, 這樣是不會有任何的偏差的
NSDecimalNumber
是 NSNumber
的一個子類, 比 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)
複製代碼
NSDecimalNumber
與 Decimal
基本是無縫橋接的, Decimal
是一個值類型 Struct
, NSDecimalNumber
是一個引用類型 Class
, 看起來 NSDecimalNumber
的設置功能更爲豐富, 可是若是隻是須要對位數, 四捨五入方式有要求的話 Decimal
也徹底能夠知足, 並且性能會更好, 因此我認爲 NSDecimalNumber
僅在 Decimal
沒法實現某個功能時才做爲備用考慮.
總的來講, NSDecimalNumber
與 Decimal
的關係相似 NSString
與 String
的關係.
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)
複製代碼
答案是簡單的: 咱們使用的 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"]
}
}
複製代碼
Decimal
的初始化方式Decimal
有多種初始化方式, 咱們能夠傳入整型值, 傳入浮點型, 傳入字符串方式進行初始化, 我認爲正確的初始化方式應該是使用字符串.
上面這張圖應該很簡單明瞭的說明了我爲何這麼認爲了. 其緣由與上個反序列問題類似, 也是由於咱們傳入 Double
時, Swift 對其進行了一次承載, 這一次承載就對其形成了精度丟失, 根據已經丟失精度的 Double
初始化出 Decimal
, 這個 Decimal
是精度丟失的也就不難理解了