做者:Soroush Khanlou,原文連接,原文日期:2016-04-08
譯者:Lanford3_3;校對:pmst;定稿:CMBgit
使用 Swift 解析 JSON 是件很痛苦的事。你必須考慮多個方面:可選類性、類型轉換、基本類型(primitive types)、構造類型(constructed types)(其構造器返回結果也是可選類型)、字符串類型的鍵(key)以及其餘一大堆問題。github
對於強類型(well-typed)的 Swift 來講,其實更適合使用一種強類型的有線格式(wire format)。在個人下一個項目中,我將會選擇使用 Google 的 protocol buffers(這篇文章說明了它的好處)。我但願在獲得更多經驗後,寫篇文章說說它和 Swift 配合起來有多麼好用。但目前這篇文章主要是關於如何解析 JSON 數據 —— 一種被最普遍使用的有線格式。json
對於 JSON 的解析,已經有了許多優秀的解決方案。第一個方案,使用如 Argo 這樣的庫,採用函數式操做符來柯里化一個初始化構造器:swift
extension User: Decodable { static func decode(j: JSON) -> Decoded<User> { return curry(User.init) <^> j <| "id" <*> j <| "name" <*> j <|? "email" // Use ? for parsing optional values <*> j <| "role" // Custom types that also conform to Decodable just work <*> j <| ["company", "name"] // Parse nested objects } }
Argo 是一個很是好的解決方案。它簡潔,靈活,表達力強,但柯里化以及奇怪的操做符都是些不太好理解的東西。(Thoughtbot 的人已經寫了一篇不錯的文章來對這些加以解釋)數組
另一個常見的解決方案是,手動使用 guard let
進行處理以獲得非可選值。這個方案須要手動作的事兒會多一些,對於每一個屬性的處理都須要兩行代碼:一行用來在 guard 語句中生成非可選的局部變量,另外一行設置屬性。若要獲得上例中一樣的結果,代碼可能長這樣:服務器
class User { init?(dictionary: [String: AnyObject]?) { guard let dictionary = dictionary, let id = dictionary["id"] as? String, let name = dictionary["name"] as? String, let roleDict = dictionary["role"] as? [String: AnyObject], let role = Role(dictionary: roleDict) let company = dictionary["company"] as? [String: AnyObject], let companyName = company["name"] as? String, else { return nil } self.id = id self.name = name self.role = role self.email = dictionary["email"] as? String self.companyName = companyName } }
這份代碼的好處在於它是純 Swift 的,不過看起來比較亂,可讀性不佳,變量間的依賴鏈並不明顯。舉個例子,因爲 roleDict
被用在 role
的定義中,因此它必須在 role
被定義前定義,但因爲代碼如此繁雜,很難清晰地找出這種依賴關係。閉包
(我甚至都不想提在 Swift 1 中解析 JSON 時,大量 if let
嵌套而成的鞭屍金字塔(pyramid-of-doom),那可真是糟透了,很高興如今咱們有了多行的 if let
和 guard let
結構。)app
在 Swift 的錯誤處理發佈的時候,我以爲這東西糟透了。彷佛無論從哪個方面都不及 Result
:異步
你沒法直接訪問到錯誤:Swift 的錯誤處理機制在 Result
類型之上,添加了一些必須使用的語法(是的,事實如此),這讓人們沒法直接訪問到錯誤。async
你不能像使用 Result
同樣進行鏈式處理。Result
是個 monad,能夠用 flatMap
連接起來進行有效的處理。
Swift 錯誤模型沒法異步使用(除非你進行一些 hack,好比說提供一個內部函數來拋出結果), 但 Result
能夠。
儘管 Swift 的錯誤處理模型有着這些看起來至關明顯的缺點,但有篇文章講述了一個使用 Swift 錯誤模型的例子,在該例子中 Swift 的錯誤模型明顯比 Objective-C 的版本更加簡潔,也比 Result
可讀性更強。這是怎麼回事呢?
這裏的祕密在於,當你的代碼中有許多 try
調用的時候,利用帶有 do
/catch
結構的 Swift 錯誤模型進行處理,效果會很是好。在 Swift 中對代碼進行錯誤處理時須要寫一些模板代碼。在聲明函數時,你須要加入 throws
, 或使用 do
/catch
結構顯式地處理全部錯誤。對於單個 try
語句來講,作這些事讓人以爲很麻煩。然而,就多個 try
語句而言,這些前期工做就變得物有所值了。
我曾試圖尋找一種方法,可以在 JSON 缺失某個鍵時打印出某種警告。若是在訪問缺失的鍵時,可以獲得一個報錯,那麼這個問題就解決了。因爲在鍵缺失的時候,原生的 Dictionary
類型並不會拋出錯誤,因此須要有個對象對字典進行封裝。我想實現的代碼大概長這樣:
struct MyModel { let aString: String let anInt: Int init?(dictionary: [String: AnyObject]?) { let parser = Parser(dictionary: dictionary) do { self.aString = try parser.fetch("a_string") self.anInt = try parser.fetch("an_int") } catch let error { print(error) return nil } } }
理想的說來,因爲類型推斷的存在,在解析過程當中我甚至不須要明確地寫出類型。如今讓咱們絲分縷解,看看怎麼實現這份代碼。首先從 ParserError
開始:
struct ParserError: ErrorType { let message: String }
接下來,咱們開始搞定 Parser
。它能夠是一個 struct
或是一個 class
。(因爲它不會被用在別的地方,因此他的引用語義並不重要。)
struct Parser { let dictionary: [String: AnyObject]? init(dictionary: [String: AnyObject]?) { self.dictionary = dictionary } }
咱們的 parser 將會獲取一個字典並持有它。
fetch
函數開始顯得有點複雜了。咱們來一行一行地進行解釋。類中的每一個方法均可以類型參數化,以充分利用類型推斷帶來的便利。此外,這個函數會拋出錯誤,以使咱們可以得到處理失敗的數據:
func fetch<T>(key: String) throws -> T {
下一步是獲取鍵對應的對象,並保證它不是空的,不然拋出一個錯誤。
let fetchedOptional = dictionary?[key] guard let fetched = fetchedOptional else { throw ParserError(message: "The key \"\(key)\" was not found.") }
最後一步是,給得到的值加上類型信息。
guard let typed = fetched as? T else { throw ParserError(message: "The key \"\(key)\" was not the correct type. It had value \"\(fetched).\"") }
最終,返回帶類型的非空值。
return typed }
(我將會在文末附上包含全部代碼的 gist 和 playground)
這份代碼是可用的!類型參數化及類型推斷爲咱們處理了一切。上面寫的 「理想」 代碼完美地工做了:
self.aString = try parser.fetch("a_string")
我還想添加一些東西。首先,添加一種方法來解析出那些確實可選的值(譯者注:也就是咱們容許這些值爲空)。因爲在這種狀況下咱們並不須要拋出錯誤,因此咱們能夠實現一個簡單許多的方法。但很不幸,這個方法沒法和上面的方法同名,不然編譯器就沒法知道應該使用哪一個方法了,因此,咱們把它命名爲 fetchOptional
。這個方法至關的簡單。
func fetchOptional<T>(key: String) -> T? { return dictionary?[key] as? T }
(若是鍵存在,可是並不是你所指望的類型,則能夠拋出一個錯誤。爲了簡略起見,我就不寫了)
另一件事就是,在字典中取出一個對象後,有時須要對它進行一些額外的轉換。咱們可能獲得一個枚舉的 rawValue
,須要構建出對應的枚舉,或者是一個嵌套的字典,須要處理它包含的對象。咱們能夠在 fetch
函數中接收一個閉包做爲參數,做進一步地類型轉換,並在轉換失敗的狀況下拋出錯誤。泛型中 U
參數類型可以幫助咱們明確 transformation
閉包轉換獲得的結果值類型和 fetch
方法獲得的值類型一致。
func fetch<T, U>(key: String, transformation: (T) -> (U?)) throws -> U { let fetched: T = try fetch(key) guard let transformed = transformation(fetched) else { throw ParserError(message: "The value \"\(fetched)\" at key \"\(key)\" could not be transformed.") } return transformed }
最後,咱們但願 fetchOptional
也能接受一個轉換閉包做爲參數。
func fetchOptional<T, U>(key: String, transformation: (T) -> (U?)) -> U? { return (dictionary?[key] as? T).flatMap(transformation) }
看啊!flatMap
的力量!注意,轉換閉包 transformation
和 flatMap
接收的閉包有着同樣的形式:T -> U?
如今咱們能夠解析帶有嵌套項或者枚舉的對象了。
class OuterType { let inner: InnerType init?(dictionary: [String: AnyObject]?) { let parser = Parser(dictionary: dictionary) do { self.inner = try parser.fetch("inner") { InnerType(dictionary: $0) } } catch let error { print(error) return nil } } }
再一次注意到,Swift 的類型推斷魔法般地爲咱們處理了一切,而咱們根本不須要寫下任何 as?
邏輯!
用相似的方法,咱們也能夠處理數組。對於基本數據類型的數組,fetch
方法已經能很好地工做了:
let stringArray: [String] //... do { self.stringArray = try parser.fetch("string_array") //...
對於咱們想要構建的特定類型(Domain Types)的數組, Swift 的類型推斷彷佛沒法那麼深刻地推斷類型,因此咱們必須加入另外的類型註解:
self.enums = try parser.fetch("enums") { (array: [String]) in array.flatMap(SomeEnum(rawValue: $0)) }
因爲這行顯得有些粗糙,讓咱們在 Parser
中建立一個新的方法來專門處理數組:
func fetchArray<T, U>(key: String, transformation: T -> U?) throws -> [U] { let fetched: [T] = try fetch(key) return fetched.flatMap(transformation) }
這裏使用 flatMap 來幫助咱們移除空值,減小了代碼量:
self.enums = try parser.fetchArray("enums") { SomeEnum(rawValue: $0) }
末尾的這個閉包應該被做用於 每一個 元素,而不是整個數組(你也能夠修改 fetchArray
方法,以在任意值沒法被構建時拋出錯誤。)
我很喜歡泛型模式。它很簡單,可讀性強,並且也沒有複雜的依賴(這只是個 50 行的 Parser 類型)。它使用了 Swift 風格的結構, 還會給你很是特定的錯誤提示,告訴你 爲什麼 解析失敗了,當你在從服務器返回的 JSON 沼澤中摸爬滾打時,這顯得很是有用。最後,用這種方法解析的另一個好處是,它在結構體和類上都能很好地工做,這使得從引用類型切換到值類型,或者反之,都變得很簡單。
這裏是包含全部代碼的一個 gist,而這裏是一個做爲補充的 Playground.
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。