使用 Swift 進行 JSON 解析

作者:Soroush Khanlou硫眯,原文鏈接乓旗,原文日期:2016-04-08
譯者:Lanford3_3禽最;校對:pmst;定稿:CMB

使用 Swift 解析 JSON 是件很痛苦的事。你必須考慮多個方面:可選類性先嬉、類型轉(zhuǎn)換湘今、基本類型(primitive types)、構(gòu)造類型(constructed types)(其構(gòu)造器返回結(jié)果也是可選類型)塘娶、字符串類型的鍵(key)以及其他一大堆問題。

對于強類型(well-typed)的 Swift 來說,其實更適合使用一種強類型的有線格式(wire format)密任。在我的下一個項目中,我將會選擇使用 Google 的 protocol buffers這篇文章說明了它的好處)偷俭。我希望在得到更多經(jīng)驗后浪讳,寫篇文章說說它和 Swift 配合起來有多么好用。但目前這篇文章主要是關(guān)于如何解析 JSON 數(shù)據(jù) —— 一種被最廣泛使用的有線格式涌萤。

對于 JSON 的解析淹遵,已經(jīng)有了許多優(yōu)秀的解決方案。第一個方案负溪,使用如 Argo 這樣的庫透揣,采用函數(shù)式操作符來柯里化一個初始化構(gòu)造器:

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 的人已經(jīng)寫了一篇不錯的文章來對這些加以解釋)

另外一個常見的解決方案是侍咱,手動使用 guard let 進行處理以得到非可選值。這個方案需要手動做的事兒會多一些密幔,對于每個屬性的處理都需要兩行代碼:一行用來在 guard 語句中生成非可選的局部變量楔脯,另一行設(shè)置屬性。若要得到上例中同樣的結(jié)果胯甩,代碼可能長這樣:

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 被定義前定義弄诲,但由于代碼如此繁雜,很難清晰地找出這種依賴關(guān)系。

(我甚至都不想提在 Swift 1 中解析 JSON 時齐遵,大量 if let 嵌套而成的鞭尸金字塔(pyramid-of-doom)寂玲,那可真是糟透了,很高興現(xiàn)在我們有了多行的 if letguard let 結(jié)構(gòu)梗摇。)


Swift 的錯誤處理發(fā)布的時候拓哟,我覺得這東西糟透了。似乎不管從哪一個方面都不及 Result

  • 你無法直接訪問到錯誤:Swift 的錯誤處理機制在 Result 類型之上伶授,添加了一些必須使用的語法(是的断序,事實如此),這讓人們無法直接訪問到錯誤糜烹。
  • 你不能像使用 Result 一樣進行鏈式處理违诗。Result 是個 monad,可以用 flatMap 鏈接起來進行有效的處理疮蹦。
  • Swift 錯誤模型無法異步使用(除非你進行一些 hack诸迟,比如說提供一個內(nèi)部函數(shù)來拋出結(jié)果), 但 Result 可以愕乎。

盡管 Swift 的錯誤處理模型有著這些看起來相當明顯的缺點阵苇,但有篇文章講述了一個使用 Swift 錯誤模型的例子,在該例子中 Swift 的錯誤模型明顯比 Objective-C 的版本更加簡潔感论,也比 Result 可讀性更強绅项。這是怎么回事呢?

這里的秘密在于笛粘,當你的代碼中有許多 try 調(diào)用的時候趁怔,利用帶有 do/catch 結(jié)構(gòu)的 Swift 錯誤模型進行處理湿硝,效果會非常好薪前。在 Swift 中對代碼進行錯誤處理時需要寫一些模板代碼。在聲明函數(shù)時关斜,你需要加入 throws示括, 或使用 do/catch 結(jié)構(gòu)顯式地處理所有錯誤。對于單個 try 語句來說痢畜,做這些事讓人覺得很麻煩垛膝。然而,就多個 try 語句而言丁稀,這些前期工作就變得物有所值了吼拥。


我曾試圖尋找一種方法,能夠在 JSON 缺失某個鍵時打印出某種警告线衫。如果在訪問缺失的鍵時凿可,能夠得到一個報錯,那么這個問題就解決了。由于在鍵缺失的時候枯跑,原生的 Dictionary 類型并不會拋出錯誤惨驶,所以需要有個對象對字典進行封裝。我想實現(xiàn)的代碼大概長這樣:

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 
        }
    }
}

理想的說來敛助,由于類型推斷的存在粗卜,在解析過程中我甚至不需要明確地寫出類型。現(xiàn)在讓我們絲分縷解纳击,看看怎么實現(xiàn)這份代碼续扔。首先從 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 函數(shù)開始顯得有點復雜了。我們來一行一行地進行解釋加匈。類中的每個方法都可以類型參數(shù)化存璃,以充分利用類型推斷帶來的便利。此外雕拼,這個函數(shù)會拋出錯誤纵东,以使我們能夠獲得處理失敗的數(shù)據(jù):

func fetch<T>(key: String) throws -> T {

下一步是獲取鍵對應(yīng)的對象,并保證它不是空的啥寇,否則拋出一個錯誤偎球。

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)

這份代碼是可用的磷醋!類型參數(shù)化及類型推斷為我們處理了一切猫牡。上面寫的 “理想” 代碼完美地工作了:

self.aString = try parser.fetch("a_string")

我還想添加一些東西。首先邓线,添加一種方法來解析出那些確實可選的值(譯者注:也就是我們允許這些值為空)淌友。由于在這種情況下我們并不需要拋出錯誤,所以我們可以實現(xiàn)一個簡單許多的方法骇陈。但很不幸震庭,這個方法無法和上面的方法同名,否則編譯器就無法知道應(yīng)該使用哪個方法了你雌,所以器联,我們把它命名為 fetchOptional。這個方法相當?shù)暮唵巍?/p>

func fetchOptional<T>(key: String) -> T? {
    return dictionary?[key] as? T
}

(如果鍵存在,但是并非你所期望的類型主籍,則可以拋出一個錯誤习贫。為了簡略起見,我就不寫了)

另外一件事就是千元,在字典中取出一個對象后苫昌,有時需要對它進行一些額外的轉(zhuǎn)換。我們可能得到一個枚舉的 rawValue幸海,需要構(gòu)建出對應(yīng)的枚舉祟身,或者是一個嵌套的字典,需要處理它包含的對象物独。我們可以在 fetch 函數(shù)中接收一個閉包作為參數(shù)袜硫,作進一步地類型轉(zhuǎn)換,并在轉(zhuǎn)換失敗的情況下拋出錯誤挡篓。泛型中 U 參數(shù)類型能夠幫助我們明確 transformation 閉包轉(zhuǎn)換得到的結(jié)果值類型和 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 也能接受一個轉(zhuǎn)換閉包作為參數(shù)官研。

func fetchOptional<T, U>(key: String, transformation: (T) -> (U?)) -> U? {
    return (dictionary?[key] as? T).flatMap(transformation)
}

看盎喟摹!flatMap 的力量戏羽!注意担神,轉(zhuǎn)換閉包 transformationflatMap 接收的閉包有著一樣的形式:T -> U?

現(xiàn)在我們可以解析帶有嵌套項或者枚舉的對象了。

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? 邏輯!

用類似的方法酷宵,我們也可以處理數(shù)組亥贸。對于基本數(shù)據(jù)類型的數(shù)組,fetch 方法已經(jīng)能很好地工作了:

let stringArray: [String]

//...
do {
    self.stringArray = try parser.fetch("string_array")
//...

對于我們想要構(gòu)建的特定類型(Domain Types)的數(shù)組忧吟, Swift 的類型推斷似乎無法那么深入地推斷類型砌函,所以我們必須加入另外的類型注解:

self.enums = try parser.fetch("enums") { (array: [String]) in array.flatMap(SomeEnum(rawValue: $0)) }

由于這行顯得有些粗糙,讓我們在 Parser 中創(chuàng)建一個新的方法來專門處理數(shù)組:

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) }

末尾的這個閉包應(yīng)該被作用于 每個 元素,而不是整個數(shù)組(你也可以修改 fetchArray 方法垦沉,以在任意值無法被構(gòu)建時拋出錯誤煌抒。)

我很喜歡泛型模式。它很簡單厕倍,可讀性強寡壮,而且也沒有復雜的依賴(這只是個 50 行的 Parser 類型)。它使用了 Swift 風格的結(jié)構(gòu), 還會給你非常特定的錯誤提示况既,告訴你 為何 解析失敗了这溅,當你在從服務(wù)器返回的 JSON 沼澤中摸爬滾打時,這顯得非常有用棒仍。最后悲靴,用這種方法解析的另外一個好處是,它在結(jié)構(gòu)體和類上都能很好地工作莫其,這使得從引用類型切換到值類型癞尚,或者反之,都變得很簡單乱陡。

這里是包含所有代碼的一個 gist浇揩,而這里是一個作為補充的 Playground.

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán)憨颠,最新文章請訪問 http://swift.gg胳徽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市爽彤,隨后出現(xiàn)的幾起案子膜廊,更是在濱河造成了極大的恐慌,老刑警劉巖淫茵,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爪瓜,死亡現(xiàn)場離奇詭異,居然都是意外死亡匙瘪,警方通過查閱死者的電腦和手機铆铆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丹喻,“玉大人薄货,你說我怎么就攤上這事“郏” “怎么了谅猾?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鳍悠。 經(jīng)常有香客問我税娜,道長,這世上最難降的妖魔是什么藏研? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任敬矩,我火速辦了婚禮,結(jié)果婚禮上蠢挡,老公的妹妹穿的比我還像新娘弧岳。我一直安慰自己凳忙,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布禽炬。 她就那樣靜靜地躺著涧卵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腹尖。 梳的紋絲不亂的頭發(fā)上柳恐,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音桐臊,去河邊找鬼胎撤。 笑死,一個胖子當著我的面吹牛断凶,可吹牛的內(nèi)容都是我干的伤提。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼认烁,長吁一口氣:“原來是場噩夢啊……” “哼肿男!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起却嗡,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤舶沛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后窗价,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體如庭,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年撼港,在試婚紗的時候發(fā)現(xiàn)自己被綠了坪它。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡帝牡,死狀恐怖往毡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情靶溜,我是刑警寧澤开瞭,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站罩息,受9級特大地震影響嗤详,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扣汪,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一断楷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧崭别,春花似錦冬筒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诀姚,卻和暖如春响牛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赫段。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工呀打, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人糯笙。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓贬丛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親给涕。 傳聞我的和親對象是個殘疾皇子豺憔,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容