或許你並不需要重寫 init(from:) 方法

四娘發表於2019-03-01

Codable 作為 Swift 的特性之一也是很注重安全,也很嚴謹,但它對於“嚴謹”和“安全”的定義不一定跟別的語言一樣,這就導致了它在實際使用時總會有這樣那樣的磕磕絆絆,我們不得不重寫 init 方法去讓它跟外部環境融洽地共存。最近在工作中這樣的事情發生多了,我也就不得不想辦法去解決它。

嚴格的型別解析

最開始遇到了第一個問題就是 Bool 的解析,我們後端的介面習慣使用 01 整數去表達布林值,解析失敗之後,我第一感覺是這會不會是個 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,不應該混到一起用。

還有一個比較棘手的問題,URLinit?(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 內部開始查詢,那就嘗試一下:

或許你並不需要重寫 init(from:) 方法

成功了,那麼就回到我們最初的目的,把 URLBool 也過載掉:

或許你並不需要重寫 init(from:) 方法

並且這種過載的方法是用的是直接派發,所以我們可以控制這個函式的作用範圍:

// 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:) 函式啦!???

覺得文章還不錯的話可以關注一下我的部落格

相關文章