使用 Swift 进行 JSON 解析
使用 Swift 解析 JSON 是件很痛苦的事。你必须考虑多个方面:可选类性、类型转换、基本类型(primitive types)、构造类型(constructed types)(其构造器返回结果也是可选类型)、字符串类型的键(key)以及其他一大堆问题。 对于强类型(well-typed)的 Swift 来说,其实更适合使用一种强类型的有线格式(wire format)。在我的下一个项目中,我将会选择使用 Google 的 protocol buffers(这篇文章说明了它的好处)。我希望在得到更多经验后,写篇文章说说它和 Swift 配合起来有多么好用。但目前这篇文章主要是关于如何解析 JSON 数据 —— 一种被最广泛使用的有线格式。 对于 JSON 的解析,已经有了许多优秀的解决方案。第一个方案,使用如 Argo 这样的库,采用函数式操作符来柯里化一个初始化构造器: 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 的人已经写了一篇不错的文章来对这些加以解释) 另外一个常见的解决方案是,手动使用 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 的,不过看起来比较乱,可读性不佳,变量间的依赖链并不明显。举个例子,由于 (我甚至都不想提在 Swift 1 中解析 JSON 时,大量 在 Swift 的错误处理发布的时候,我觉得这东西糟透了。似乎不管从哪一个方面都不及
尽管 Swift 的错误处理模型有着这些看起来相当明显的缺点,但有篇文章讲述了一个使用 Swift 错误模型的例子,在该例子中 Swift 的错误模型明显比 Objective-C 的版本更加简洁,也比 这里的秘密在于,当你的代码中有许多 我曾试图寻找一种方法,能够在 JSON 缺失某个键时打印出某种警告。如果在访问缺失的键时,能够得到一个报错,那么这个问题就解决了。由于在键缺失的时候,原生的 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 } } } 理想的说来,由于类型推断的存在,在解析过程中我甚至不需要明确地写出类型。现在让我们丝分缕解,看看怎么实现这份代码。首先从 struct ParserError: ErrorType { let message: String } 接下来,我们开始搞定 struct Parser { let dictionary: [String: AnyObject]? init(dictionary: [String: AnyObject]?) { self.dictionary = dictionary } } 我们的 parser 将会获取一个字典并持有它。
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") 我还想添加一些东西。首先,添加一种方法来解析出那些确实可选的值(译者注:也就是我们允许这些值为空)。由于在这种情况下我们并不需要抛出错误,所以我们可以实现一个简单许多的方法。但很不幸,这个方法无法和上面的方法同名,否则编译器就无法知道应该使用哪个方法了,所以,我们把它命名为 func fetchOptional<T>(key: String) -> T? { return dictionary?[key] as? T } (如果键存在,但是并非你所期望的类型,则可以抛出一个错误。为了简略起见,我就不写了) 另外一件事就是,在字典中取出一个对象后,有时需要对它进行一些额外的转换。我们可能得到一个枚举的 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 } 最后,我们希望 func fetchOptional<T,transformation: (T) -> (U?)) -> U? { return (dictionary?[key] as? T).flatMap(transformation) } 看啊! 现在我们可以解析带有嵌套项或者枚举的对象了。 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 的类型推断魔法般地为我们处理了一切,而我们根本不需要写下任何 用类似的方法,我们也可以处理数组。对于基本数据类型的数组, 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)) } 由于这行显得有些粗糙,让我们在 func fetchArray<T,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) } 末尾的这个闭包应该被作用于 每个 元素,而不是整个数组(你也可以修改 我很喜欢泛型模式。它很简单,可读性强,而且也没有复杂的依赖(这只是个 50 行的 Parser 类型)。它使用了 Swift 风格的结构, 还会给你非常特定的错误提示,告诉你 为何 解析失败了,当你在从服务器返回的 JSON 沼泽中摸爬滚打时,这显得非常有用。最后,用这种方法解析的另外一个好处是,它在结构体和类上都能很好地工作,这使得从引用类型切换到值类型,或者反之,都变得很简单。 这里是包含所有代码的一个 gist,而这里是一个作为补充的 Playground.
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |