Codable 作為 Swift 的特性之一也是很注重安全,也很嚴謹,但它對於“嚴謹”和“安全”的定義不一定跟別的語言一樣,這就導致了它在實際使用時總會有這樣那樣的磕磕絆絆,我們不得不重寫 init 方法去讓它跟外部環境融洽地共存。最近在工作中這樣的事情發生多了,我也就不得不想辦法去解決它。
嚴格的型別解析
最開始遇到了第一個問題就是 Bool
的解析,我們後端的介面習慣使用 0
跟 1
整數去表達布林值,解析失敗之後,我第一感覺是這會不會是個 bug,所以去翻了一下 JSONDecoder
的原始碼:
func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
...
if let number = value as? NSNumber {
if number === kCFBooleanTrue as NSNumber {
return true
} else if number === kCFBooleanFalse as NSNumber {
return false
}
}
...
}
複製程式碼
如果把 ===
改成 ==
就可以很好地解決我的問題,我本來還很天真得以為這真的是個 bug,但在 Twitter 上向開發組的人求證之後,他們表示程式碼並沒有錯,就是這麼設計的,Boolean 就是 Boolean,Int 就是 Int,不應該混到一起用。
還有一個比較棘手的問題,URL
的 init?(string:)
在傳入空字串的時候會初始化失敗,所以在把空字串解析為 URL
的時候會直接中斷整個解析然後丟擲錯誤,還有一個就是陣列內部存在 null
元素的時候,如果 Array
的元素不宣告為 Optional
的話也是會中斷解析。
Swizzle 掉 decode 方法
比起重新自定義一個 Decoder 來說,如果能夠 swizzle 掉 decode 方法,直接控制 decode 行為會更加方便。實際上我們真的可以做到,Codable 的原理是自動程式碼生成,嚴格來說,它其實不算是編譯的一部分:
struct Foo: Codable {
var bar: Int?
// <--自動生成的部分
init(from decoder: Decoder) throws {
let container = decoder.container(keyedBy: CodingKeys.self)
bar = container.decodeIfPresent(Int.self, forKey: .bar)
}
// 自動生成的部分-->
}
複製程式碼
並且 decodeIfPresent
方法是在 Foundation 框架裡的,那麼我們能不能在我們的 Module 裡也寫一個 decodeIfPresent
方法過載掉它呢?因為如果方法是在 extension 裡宣告並實現的話,方法會優先從 Module 內部開始查詢,那就嘗試一下:
成功了,那麼就回到我們最初的目的,把 URL
和 Bool
也過載掉:
並且這種過載的方法是用的是直接派發,所以我們可以控制這個函式的作用範圍:
// A 檔案
extension KeyedDecodingContainer {
fileprivate func decodeIfPresent(_ type: Int.Type, forKey key: CodingKey) -> Int? { ... }
}
// B 檔案
// 這裡不會呼叫到 A 檔案裡的方法
let b = container.decodeIfPresent(Int.self, forKey: key)
複製程式碼
甚至我們可以在 Module 內過載一遍,應對個別特殊情況可以在檔案裡再過載一遍,達到最佳的靈活度,從某種程度上來說,我認為這甚至是比 Objective-C 的訊息機制更加靈活的一種函式宣告機制,而且它的影響範圍是有限的,不容易對外部模組造成破壞(別宣告為 open
或者 public
就不會有問題)。
我對於 Twitter 上 Swift 開發團隊的成員發的一條推印象特別深,他說其實 Swift 也有 Selector 和 IMP 的機制,只不過這個方法選擇的過程是在編譯時去完成,而並非在執行時去完成的。通過了解方法選擇的規則,就可以做到類似於 Swizzle 的效果,這也是 Swift 過載機制有趣而且複雜的地方。
總結
現在大家可以通過這種方法去重構掉專案裡那些多餘的 init(from:)
函式啦!???
覺得文章還不錯的話可以關注一下我的部落格