所有文章已搬遷到個人站點: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 其實是一個組合協議,由 Decodable
和 Encodable
兩個協議組成:
/// 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
}
Encodable
和 Decodable
分別定義了 encode(to:)
和 init(from:)
兩個協議函數,分別用來實現數據模型的歸檔和外部數據的解析和實例化。最常用的場景就是接口 JSON 數據解析和模型創建。但是 Codable 的能力并不止于此,這個后面會說。
解析 JSON 對象
先來看 Decodable
對 JSON 數據對象的解析。Swift 為我們做了絕大部分的工作,Swift 中的基本數據類型比如 String
、Int
、Float
等都已經實現了 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
。
另外可選的格式標準有 secondsSince1970
、millisecondsSince1970
、iso8601
等,這些都是有詳細說明的通用標準,不清楚的自行谷歌吧 :)
同時 Codable 提供了兩種方自定義 Date
格式的策略:
-
formatted(DateFormatter)
這種策略通過設置DateFormatter
來指定Date
格式 -
custom((Decoder) throws -> Date)
custom
策略接受一個(Decoder) -> Date
的閉包,基本上是把解析任務完全丟給我們自己去實現了,具有較高的自由度
小數解析策略
小數類型(Float
/Double
) 默認也實現了 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
、-infinity
、nan
三個特殊值的表示方式:
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 協議,因此 School
與 Student
一樣,只要所有屬性都實現了 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 協議作任何修改,只需要將 JSONEncoder
和 JSONDecoder
替換成對應的 PropertyListEncoder
和 PropertyListDecoder
即可。
屬性列表本質上是特殊格式標準的 XML
文檔,所以理論上來說,我們可以參照系統提供的 Decoder/Encoder 自己實現任意格式的數據序列化與反序列化方案。同時蘋果也隨時可能通過實現新的 Decoder/Encoder 類來擴展其他數據格式的處理能力。這也正是文章開頭所說的,Codable 的能力并不止于此,它具有很大的可擴展空間。
結語
到此 Codable 的核心用法基本講完了。相比目前比較常用的幾個框架:
ObjectMapper 使用范型機制進行模型解析,但是需要手動對每一個屬性寫映射關系,比較繁瑣。我自己項目中也是用的這個框架,后來自己對其做了些優化,利用反射機制對基本數據類型實現了自動解析,但是自定義類型仍然需要手動寫映射,并且必須繼承實現了自動解析的 Model 基類,限制較多。
SwiftyJSON 簡單了解過,其本質其實只是將 JSON 解析成了字典類型的數據,而實際使用時依然需要使用下標方式去取值,非常繁瑣且容易出錯,不易閱讀和維護,個人認為這是很糟糕的設計。
HandyJSON 是阿里推出的框架,思路與 Codable 殊途同歸,之前也用過一陣,當時因為對枚舉和 Date
等類型的支持還不夠完善,最終還是用回了ObjectMapper。不過目前看來完善程度已經很高了,或許可以再次嘗試踩下坑。
總體來說,Codable 作為語言層面對模型解析的支持方案,有其自身的優勢。不過在靈活性上稍有欠缺,對自定義字段的支持也還不夠人性化,期待后續的完善。
對于第三方庫來說,Codable 的推出既是一種挑戰,但同時也是一個機遇,相信這些框架的作者們都會從 Codaable 獲得許多靈感來優化提升自己的框架,在不久的將來制造一個百家爭鳴的局面。