Swift 4 踩坑之 Codable 協議

所有文章已搬遷到個人站點:me.harley-xk.studio,歡迎訪問留言

WWDC 過去有一段時間了,最近終于有時間空閑,可以靜下心來仔細研究一下相關內容。對于開發者來說,本屆WWDC 最重要的消息還是得屬 Swift 4 的推出。

Swift 經過三年的發展,終于在 API 層面趨于穩定。從 Swift 3 遷移代碼到 Swift 4 終于不用像 2 到 3 那樣痛苦了。這對開發者來說實在是個重大利好,應該會吸引一大批對 Swift 仍然處于觀望狀態的開發者加入。

另外 Swift 4 引入了許多新的特性,像是 fileprivate 關鍵字的限制范圍更加精確了;聲明屬性終于可以同時限制類型和協議了;新的 KeyPath API 等等,從這些改進我們可以看到,Swift 的生態越來越完善,Swift 本身也越來越強大。

而 Swift 4 帶來的新特性中,最讓人眼前一亮的,我覺得非 Codable 協議莫屬,下面就來介紹下我自己對 Codable 協議踩坑的經驗總結。

簡單介紹

Swift 由于類型安全的特性,對于像 JSON 這類弱類型的數據處理一直是一個比較頭疼的問題,雖然市面上許多優秀的第三方庫在這方面做了不少努力,但是依然存在著很多難以克服的缺陷,所以 Codable 協議的推出,一來打破了這樣的僵局,二來也給我們解決類似問題提供了新的思路。

通過查看定義可以看到,Codable 其實是一個組合協議,由 DecodableEncodable 兩個協議組成:

/// A type that can convert itself into and out of an external representation.
public typealias Codable = Decodable & Encodable

/// A type that can encode itself to an external representation.
public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}

/// A type that can decode itself from an external representation.
public protocol Decodable {
    public init(from decoder: Decoder) throws
}

EncodableDecodable 分別定義了 encode(to:)init(from:) 兩個協議函數,分別用來實現數據模型的歸檔和外部數據的解析和實例化。最常用的場景就是接口 JSON 數據解析和模型創建。但是 Codable 的能力并不止于此,這個后面會說。

解析 JSON 對象

先來看 Decodable 對 JSON 數據對象的解析。Swift 為我們做了絕大部分的工作,Swift 中的基本數據類型比如 StringIntFloat 等都已經實現了 Codable 協議,因此如果你的數據類型只包含這些基本數據類型的屬性,只需要在類型聲明中加上 Codable 協議就可以了,不需要寫任何實際實現的代碼,這也是 Codable 最大的優勢所在。

比如我們有下面這樣一個學生信息的 JSON 字符串:

let jsonString =
"""
{
    "name": "小明",
    "age": 12,
    "weight": 43.2
}
"""

這時候,只需要定義一個 Student 類型,聲明實現 Decodable 協議即可,Swift 4 已經為我們提供了默認的實現:

struct Student: Decodable {   
    var name: String
    var age: Int
    var weight: Float
}

然后,只需要一行代碼就可以將 小明 解析出來了:

let xiaoming = try JSONDecoder().decode(Student.self, from: jsonString.data(using: .utf8)!)

這里需要注意的是, decode 函數需要外部數據類型為 Data 類型,如果是字符串需要先轉換為 Data 之后操作,不過像 Alamofire 之類的網絡框架,返回數據原本就是 Data 類型的。
另外 decode 函數是標記為 throws 的,如果解析失敗,會拋出一個異常,為了保證程序的健壯性,需要使用 do-catch 對異常情況進行處理:

do {
    let xiaoming = try JSONDecoder().decode(Student.self, from: data)
} catch {
    // 異常處理
}

特殊數據類型

很多時候光靠基本數據類型并不能完成工作,往往我們需要用到一些特殊的數據類型。Swift 對許多特殊數據類型也提供了默認的 Codable 實現,但是有一些限制。

枚舉
{
    ...
    "gender": "male"
    ...
}

性別是一個很常用的信息,我們經常會把它定義成枚舉:

enum Gender {
    case male
    case female
    case other
}

枚舉類型也默認實現了 Codable 協議,但是如果我們直接聲明 Gender 枚舉支持 Codable 協議,編譯器會提示沒有提供實現:

其實這里有一個限制:枚舉類型要默認支持 Codable 協議,需要聲明為具有原始值的形式,并且原始值的類型需要支持 Codable 協議:

enum Gender: String, Decodable {
    case male
    case female
    case other
}

由于枚舉類型原始值隱式賦值特性的存在,如果枚舉值的名稱和對應的 JSON 中的值一致,不需要顯式指定原始值即可完成解析。

Bool

我們的數據模型現在新增了一個字段,用來表示某個學生是否是少先隊員:

{
    ...
    "isYoungPioneer": true
    ...
}

這時候,直接聲明對應的屬性就可以了:

var isYoungPioneer: Bool

Bool 類型原本沒什么好講的,不過因為踩到了坑,所以還是得說一說:
目前發現的坑是:Bool 類型默認只支持 true/false 形式的 Bool 值解析。對于一些使用 0/1 形式來表示 Bool 值的后端框架,只能通過 Int 類型解析之后再做轉換了,或者可以自定義實現 Codable 協議。

日期解析策略

說了枚舉和 Bool,另外一個常用的特殊類型就是 Date 了,Date 類型的特殊性在于它有著各種各樣的格式標準和表示方式,從數字到字符串可以說是五花八門,解析 Date 類型是任何一個同類型的框架都必須面對的課題。

對此,Codable 給出的解決方案是:定義解析策略。JSONDecoder 類聲明了一個 DateDecodingStrategy 類型的屬性,用來制定 Date 類型的解析策略,同樣先看定義:

/// The strategy to use for decoding `Date` values.
public enum DateDecodingStrategy {
    
    /// Defer to `Date` for decoding. This is the default strategy.
    case deferredToDate
    
    /// Decode the `Date` as a UNIX timestamp from a JSON number.
    case secondsSince1970
    
    /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
    case millisecondsSince1970
    
    /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
    case iso8601
    
    /// Decode the `Date` as a string parsed by the given formatter.
    case formatted(DateFormatter)
    
    /// Decode the `Date` as a custom value decoded by the given closure.
    case custom((Decoder) throws -> Date)
}

Codable 對幾種常用格式標準進行了支持,默認啟用的策略是 deferredToDate,即從 **UTC 時間2001年1月1日 **開始的秒數,對應 Date 類型中 timeIntervalSinceReferenceDate 這個屬性。比如 519751611.125429 這個數字解析后的結果是 2017-06-21 15:26:51 +0000

另外可選的格式標準有 secondsSince1970millisecondsSince1970iso8601 等,這些都是有詳細說明的通用標準,不清楚的自行谷歌吧 :)

同時 Codable 提供了兩種方自定義 Date 格式的策略:

  • formatted(DateFormatter)
    這種策略通過設置 DateFormatter 來指定 Date 格式
  • custom((Decoder) throws -> Date)
    custom 策略接受一個 (Decoder) -> Date 的閉包,基本上是把解析任務完全丟給我們自己去實現了,具有較高的自由度
小數解析策略

小數類型(FloatDouble) 默認也實現了 Codable 協議,但是小數類型在 Swift 中有許多特殊值,比如圓周率(Float.pi)等。這里要說的是另外兩個屬性,先看定義:

/// Positive infinity.
///
/// Infinity compares greater than all finite numbers and equal to other
/// infinite values.
public static var infinity: Double { get }

/// A quiet NaN ("not a number").
///
/// A NaN compares not equal, not greater than, and not less than every
/// value, including itself. Passing a NaN to an operation generally results
/// in NaN.
public static var nan: Double { get }

infinity 表示正無窮(負無窮寫作:-infinity),nan 表示沒有值,這些特殊值沒有辦法使用數字進行表示,但是在 Swift 中它們是確確實實的值,可以參與計算、比較等。
不同的語言、框架對此會有類似的實現,但是表達方式可能不完全相同,因此如果在某些場景下需要解析這樣的值,就需要做特殊轉換了。

Codable 的實現方式比較簡單粗暴,JSONDecoder 類型有一個屬性 nonConformingFloatDecodingStrategy ,用來指定不一致的小數轉換策略,默認值為 throw, 即直接拋出異常,解析失敗。另外一個選擇就是自己指定 infinity-infinitynan 三個特殊值的表示方式:

let decoder = JSONDecoder()
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "infinity", negativeInfinity: "-infinity", nan: "nan")
// 另外一種表示方式
// decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "∞", negativeInfinity: "-∞", nan: "n/a")

目前看來只支持這三個特殊值的轉換,不過這種特殊值的使用場景應該非常有限,至少在我自己五六年的開發生涯中還沒有遇到過。

自定義數據類型

純粹的基本數據類型依然不能很好地工作,實際項目的數據結構往往是很復雜的,一個數據類型經常會包含另一個數據類型的屬性。比如說我們這個例子中,每個學生信息中還包含了所在學校的信息:

{
    "name": "小明",
    "age": 12,
    "weight": 43.2
    "school": {
      "name": "市第一中學",
      "address": "XX市人民中路 66 號"
    }
}

這時候就需要 Student 和 School 兩個類型來組合表示:

struct School: Decodable {
    var name: String
    var address: String
}
struct Student: Decodable {   
    var name: String
    var age: Int
    var weight: Float
    var school: School
}

由于所有基本類型都實現了 Codable 協議,因此 SchoolStudent 一樣,只要所有屬性都實現了 Codable 協議,就不需要手動提供任何實現即可獲得默認的 Codable 實現。由于 School 支持了 Codable 協議,保證了 Student 依然能夠獲得默認的 Codable 實現,因此,嵌套類型的解析同樣不需要額外的代碼了。

自定義字段

很多時候前后端不一定能完全步調一致,觀念相同。所以往往后端給出的數據結構中會有一些比較個性的字段名,當然有時候是我們自己。另外有一些框架(比如我正在用的 Laravel)習慣使用蛇形命名法,而 iOS 的代碼規范推薦使用駝峰命名法,為了保證代碼風格和平臺特色,這時候就必須要自行指定字段名了。

在研究自定義字段之前我們需要深入底層,了解下 Codable 默認是怎么實現屬性的名稱識別及賦值的。通過研究底層的 C++ 源代碼可以發現,Codable 通過巧(kai)妙(guà)的方式,在編譯代碼時根據類型的屬性,自動生成了一個 CodingKeys 的枚舉類型定義,這是一個以 String 類型作為原始值的枚舉類型,對應每一個屬性的名稱。然后再給每一個聲明實現 Codable 協議的類型自動生成 init(from:)encode(to:) 兩個函數的具體實現,最終完成了整個協議的實現。

所以我們可以自己實現 CodingKeys 的類型定義,并且給屬性指定不同的原始值來實現自定義字段的解析。這樣編譯器會直接采用我們已經實現好的方案而不再重新生成一個默認的。

比如 Student 需要增加一個出生日期的屬性,后端接口使用蛇形命名,JSON 數據如下:

{
    "name": "小明",
    "age": 12,
    "weight": 43.2
    "birth_date": "1992-12-25"
}

這時候在 Student 類型聲明中需要增加 CodingKeys 定義,并且將 birthday 的原始值設置為 birth_date

struct Student: Codable {
    ...
    var birthday: Date
    
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case weight
        case birthday = "birth_date"
    }
}

需要注意的是,即使屬性名稱與 JSON 中的字段名稱一致,如果自定義了 CodingKeys,這些屬性也是無法省略的,否則會得到一個 Type 'Student' does not conform to protocol 'Codable' 的編譯錯誤,這一點還是有點坑的。不過在編譯時給 CodingKeys 補全其他默認的屬性的聲明在理論上是可行的,期待蘋果后續的優化了。

可選值

有些字段有可能會是空值。還是用學生的出生日期來舉例,假設有些學生的出生日期沒有統計到,這時候后臺返回數據格式有兩種選擇,一種是對于沒有出生日期的數據,直接不包含 birth_date 字段,另一種是指定為空值:"birth_date": null

對于這兩種形式,都只需要將 birthday 屬性聲明為可選值即可正常解析:

...
var birthday: Date?
...

解析 JSON 數組

Codable 協議同樣支持數組類型,只需要滿足一個前提:只要數組中的元素實現了 Codable 協議,數組將自動獲得 Codable 協議的實現。

使用 JSONDecoder 解析時只需要指定類型為對應的數組即可:

do {
    let students = try JSONDecoder().decode([Student].self, from: data)
} catch {
    // 異常處理
}

歸檔數據

歸檔數據使用 Encodable 協議,使用方式與 Decodable 一致。

導出為 JSON

將數據模型轉換為 JSON 與解析過程類似,將 JSONDecoder 更換為 JSONEncoder 即可:

let data = try JSONEncoder().encode(xiaomin)
let json = String(data: data, encoding: .utf8)

JSONEncoder 有一個 outputFormatting 的屬性,可以指定輸出 JSON 的排版風格,看定義:

public enum OutputFormatting {
    
    /// Produce JSON compacted by removing whitespace. This is the default formatting.
    case compact
    
    /// Produce human-readable JSON with indented output.
    case prettyPrinted
}
  • compact

    默認的 compact 風格會移除 JSON 數據中的所有格式信息,比如換行、空格和縮緊等,以減小 JSON 數據所占的空間。如果導出的 JSON 數據用戶程序間的通訊,對閱讀要求不高時,推薦使用這個設置。

  • prettyPrinted

    如果輸出的 JSON 數據是用來閱讀查看的,那么可以選擇 prettyPrinted,這時候輸出的 JSON 會自動進行格式化,添加換行、空格和縮進,以便于閱讀。類似于上面文中使用的 JSON 排版風格。

屬性列表(PropertyList)

Codable 協議并非只支持 JSON 格式的數據,它同樣支持屬性列表,即 mac 上常用的 plist 文件格式。這在我們做一些系統配置之類的工作時會很有用。

屬性列表的解析和歸檔秉承了蘋果API一貫的簡潔易用的特點,使用方式 JSON 格式一致,并不需要對已經實現的 Codable 協議作任何修改,只需要將 JSONEncoderJSONDecoder 替換成對應的 PropertyListEncoderPropertyListDecoder 即可。

屬性列表本質上是特殊格式標準的 XML 文檔,所以理論上來說,我們可以參照系統提供的 Decoder/Encoder 自己實現任意格式的數據序列化與反序列化方案。同時蘋果也隨時可能通過實現新的 Decoder/Encoder 類來擴展其他數據格式的處理能力。這也正是文章開頭所說的,Codable 的能力并不止于此,它具有很大的可擴展空間。

結語

到此 Codable 的核心用法基本講完了。相比目前比較常用的幾個框架:

ObjectMapper 使用范型機制進行模型解析,但是需要手動對每一個屬性寫映射關系,比較繁瑣。我自己項目中也是用的這個框架,后來自己對其做了些優化,利用反射機制對基本數據類型實現了自動解析,但是自定義類型仍然需要手動寫映射,并且必須繼承實現了自動解析的 Model 基類,限制較多。

SwiftyJSON 簡單了解過,其本質其實只是將 JSON 解析成了字典類型的數據,而實際使用時依然需要使用下標方式去取值,非常繁瑣且容易出錯,不易閱讀和維護,個人認為這是很糟糕的設計。

HandyJSON 是阿里推出的框架,思路與 Codable 殊途同歸,之前也用過一陣,當時因為對枚舉和 Date 等類型的支持還不夠完善,最終還是用回了ObjectMapper。不過目前看來完善程度已經很高了,或許可以再次嘗試踩下坑。

總體來說,Codable 作為語言層面對模型解析的支持方案,有其自身的優勢。不過在靈活性上稍有欠缺,對自定義字段的支持也還不夠人性化,期待后續的完善。

對于第三方庫來說,Codable 的推出既是一種挑戰,但同時也是一個機遇,相信這些框架的作者們都會從 Codaable 獲得許多靈感來優化提升自己的框架,在不久的將來制造一個百家爭鳴的局面。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,040評論 6 13
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,775評論 18 139
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,142評論 4 61
  • 一天當中,我們起碼應該,擠出十分鐘的寧靜,讓自己有喘一口氣的閑暇,有一個可以讓陽光,照進來的間隙。至少給自己十分鐘...
    FAB小涵閱讀 138評論 0 0
  • 這句話說的冷曦爸爸的心里一陣暖流流過,他一直在笑,這下葉子也放下心來了也不拘謹了。這讓冷曦的心里松了一口氣,晚...
    丶葉子丶丿閱讀 247評論 0 0