Swift 4.1 新特性 (4) Codable的改進

在 Swift 4.0 的標準庫中,引入了 Codable 接口,它實際上是 Encodable & Decodable 兩個接口的複合接口。感謝編譯器的加持,可以很方便地合成 init(from:Decoder) 以及 encode(to:Encoder) 這兩個函數。Swift 4.1 爲 JSONEncoder 和 JSONDecoder 分別引入了兩個新的屬性: keyEncodingStrategy 以及 keyDecodingStrategy,來詳細瞭解一下用途。

1. 彌合下劃線命名和駝峯命名

在日常使用過程中,我們經常會碰到強類型對象和 JSON 不同命名風格轉換的問題,來看下面的例子。

let jsonStr = """
{
"age": 18,
"first_name": "Leon"
}
"""

struct Person : Codable {
  var age: Int
  var firstName: String
}

在這個例子中,JSON 字符串無法解碼成 Person,同時 Person 的實例也無法編碼成這個 JSON 字符串。原因就在於第二個屬性的命名風格是不同的,前者使用了下劃線命名法 (Snake Case),後者使用了駝峯命名法 (Camel Case)。爲了解決這個問題,在Swift 4.0 中,我們需要在 Person 內部自定義一個 CodingKeys,如下:

enum CodingKeys : String, CodingKey {
    case age
    case firstName = "first_name"
}

這個內部枚舉 CodingKeys 的 rawType 是 String,並且聲明實現 CodingKey 接口。這時,編譯器會使用它的信息合成 Codable 相關函數,包括 Person.CodingKeys 的完整實現,問題解決。而在Swift 4.1 中,解決這個問題有了個更方便的方法:不指定 CodingKeys,而是在編碼的時候,把 JSONEncoder 的屬性 keyEncodingStrategy 設置爲 .convertToSnakeCase;在解碼的時候,把 JSONDecoder 的屬性 keyDecodingStrategy 設置成 .convertFromSnakeCase。代碼如下:

// 編碼
let leon = Person(age: 18, firstName: "Leon")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let resultData = try? encoder.encode(leon)

// 解碼
let data = jsonStr.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let person = try? decoder.decode(Person.self, from: data)

2. 自定義鍵策略

大家一定想到了,上面這個例子只解決了最典型的一種 Key 風格不匹配的情況。有很多其它情況需要覆蓋,比如:首字母大寫的帕斯卡命名法瞭解一下?

let jsonStr = """
{
"Age": 18,
"FirstName": "Leon"
}
"""

實際上,我們需要更通用的方法,來解決 JSON 解碼和編碼的鍵策略問題。它就是 keyEncodingStrategy 和 keyDecodingStrategy 的另一個枚舉項 .custom(([CodingKey]) -> CodingKey>),我們看到它接受一個 CodingKey 數組到 CodingKey 的函數作爲關聯值。

那麼 CodingKey 到底是什麼呢?官方這樣定義:一種被當做鍵 (Key) 用於編碼和解碼的類型。它是個 protocol,定義了以下四個方法,簡寫如下:

protocol CodingKey {
  var stringValue: String { get }
  init?(stringValue: String)

  var intValue: Int? { get }
  init?(intValue: Int)
}

我們看到:

  1. 可劃分爲 stringValue 和 intValue 兩對,分別代表以 String 爲鍵和以 Int 爲鍵的兩種情況。
  2. 在每一種對方法中,定義對 String / Int 的雙向轉換。
  3. 初始化函數是 failable 的。
  4. stringValue 不是 Optional 的,intValue 是 Optional 的。

回到需要解決的問題,我們需要傳入的這個 ([CodingKey]) -> CodingKey> 函數,正是利用了 CodingKey 可以雙向轉換的特性,將一個 CodingKey 轉換成另一種 CodingKey,所以實質上提供的是一個 map 函數。 看到這裏,可能你有些疑惑,輸入參數可是個數組,這裏解釋一下:之所以是數組是爲了提供給你到當前編解碼位置的完整路徑,在大多數情況下,我們只需要取數組最後一個 CodingKey 即可。

結合之前的例子解決方案如下:默認情況在編碼成 JSON 的時候,Encoder 會使用 Person.CodingKeys 進行編碼,調用它的 stringValue,最終給出在實際 JSON 中當做鍵的字符串(駝峯命名風格的)。當使用 .custom 鍵編碼策略的時候,就在上述過程中插入了一步:將 Person.CodingKeys 轉變成了另一個 CodingKey(下面實現中的 SimpleCodingKey ),在負責轉換的 map 函數中,將 stringValue 從 Person.CodingKeys 對象中取出,首字母變成大寫,再用來構造 SimpleCodingKey,這時候實際用以 JSON 編碼的 CodingKey 被替換成 SimpleCodingKey了(帕斯卡命名風格)。實現代碼如下:

struct SimpleCodingKey : CodingKey {
    var stringValue: String
    var intValue: Int?
    
    init(stringValue: String) {
        self.stringValue = stringValue
    }
    
    init(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }
}

extension JSONEncoder.KeyEncodingStrategy {
    static var convertToPascalCase: JSONEncoder.KeyEncodingStrategy {
        return .custom { codingKeys in
            var str = codingKeys.last!.stringValue
            guard let firstChar = str.first else {
                return SimpleCodingKey(stringValue: str)
            }
            let startIdx = str.startIndex
            str.replaceSubrange(startIdx...startIdx,
                                with: String(firstChar).uppercased())
            return SimpleCodingKey(stringValue: str)
        }
    }
}

let leon = Person(age: 18, firstName: "Leon")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToPascalCase
let resultData = try? encoder.encode(leon)

這裏有兩處實現細節要注意一下:

  1. SimpleCodingKey 的初始化方法不是 failable 的,但根據實現的要求,非 failable 的初始化方法被認定爲實現了接口中的 failable 初始化方法
  2. extension 中使用了語法糖,添加的 convertToPascalCase 實際上是個 static var,但可以用類似於 enum 實例的語法來使用。

以上解決了編碼部分,其實解碼部分也是類似的,區別在於代碼中的 uppercased 換成 lowercased。CodingKeys 的轉換過程可以被描述爲:_JSONKey 的對象需要轉換成首字母小寫的 SimpleCodingKey ,然後 SimpleCodingKey 再被拿去匹配 Person.CodingKeys,這個函數大家可以嘗試自己實現一下。

3. 設計亮點

在瞭解了新特性的基礎和高級用法之後,這裏想簡單提一下 Codable 的設計亮點,從設計者的角度學習和了解下。

  1. 基於接口的面向對象設計:從微觀來看 CodingKey 接口的穩定設計抽象,使得 Swift 4.1 中加入 .custom CodingKey 的轉換成爲可能。從宏觀來看 Encoder 和 Decoder,乃至 Codable 都是基於接口的面向對象設計,這使得整套設計是獨立於具體格式的,標準庫中內置了 JSON 和 PropertyList 兩種編解碼器,而你可以通過實現自己的 Encoder 和 Decoder,支持新的格式。
  2. 關聯值枚舉作爲配置屬性:KeyEncodingStrategy 和 KeyDecodingStrategy 中的.custom(([CodingKey]) -> CodingKey>),讓策略以一種優雅的方式得以動態配置。類似的設計我們還可以從 dateEncodingStrategy 等地方看到。
  3. 泛型設計和元類型編程:我們在解碼JSON的時候是這麼寫的:try? decoder.decode(Person.self, from: data)這裏傳入 Person.self 其實就涉及到了元類型編程了。這個函數是原型是:func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable,如果要展開說,就是另一個話題了。我們可以在瞭解元類型設計的時候,想到這個範本。

小結

Codable在 Swift 4.0 的引入以及在 Swift 4.1 中的加強給 Swift 強類型持久化方案提供了新的技術選型,而且是第一梯隊的。除了上述所提到的設計亮點,它還有親兒子獨有的編譯器合成特性,以及存在於標準庫的待遇,而後者的意義對於許多框架和庫的作者來說,足以使之成爲首選方案。

在本文中,我們討論了:

  • 使用新的配置屬性彌合下劃線命名和駝峯命名
  • CodingKey 設計和自定義 CodingKey 轉換
  • 簡單描述了 Codable 的設計亮點
  • 我們應該將 Codable 納入 Swift 持久化方案的優先技術選型

Swift 4.1 新特性系列文章

Swift 4.1 新特性 (1) Conditional Conformance
Swift 4.1 新特性 (2) Sequence.compactMap
Swift 4.1 新特性 (3) 合成 Equatable 和 Hashable
Swift 4.1 新特性 (4) Codable的改進

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章