做者: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。