微信号:swift-cafe

介绍:喝杯咖啡,聊聊技术,小而美的技术自媒体. 探讨 Swift;Objective-C;Cocoa 等开发技术.

用 Codable 协议实现快速 JSON 解析

2018-02-11 10:48 swift

如果你是一名有一定开发经验的开发者,那么你就一定会遇到过数据解析的问题。 最常见的就是 JSON 数据的解析,你的 APP 总会要请求一些服务器数据,比如各种信息列表,配置数据等。 

如果你之前用过 Objective-C 的话, 那么你一定对 NSJSONSerialization 并不陌生。 它的总体步骤大致是这样,先从 Data 对象中解析出 NSDictionary 或 NSArray, 然后在从这里面按照属性名称取出需要的值,最后再用这些值给实体对象赋值。

总体来说这个操作并不轻松,而且比较容易出差错,比如你在写解析代码的时候把属性名写错了,或者某个 nil 判断没有处理正确,导致了程序意外崩溃,就需要花不少时间进行调试。

Codable

我们的主题自然不是 NSJSONSerialization, 而是 Swift 中提供的 Codable 协议。 它和前者有着相似的作用,但应用范围更广,并且易用性更好。 先来看一下 Codable 协议的定义:

typealias Codable = Decodable & Encodable

它其实另外两个 Protocol 的集合,也就是 Decodable 和 Encodable。 一个用作数据解析,另一个用作数据编码。 其他不多说,咱们先来看一个实例,我们先声明一个实体类 Person 它声明实现了 Codable

struct Person : Codable {

    var name: String
    var gender: String
    var age: Int

}

除了声明 Codable 之外,这个实体类并没有其他代码,只有几个属性声明。 如果我们需要把他的实例编码成 JSON 字符串,可以这样:

let person = Person(name: "swift", gender: "male", age: 24)

let encoder = JSONEncoder()
let data = try! encoder.encode(person)
let encodedString = String(data: data, encoding: .utf8)!
print(encodedString)   // 输出 { "name": "swift", "age": 24, "gender": "male"}

如上所示,首先初始化了一个 Person 实例。 然后初始化了一个 JSONEncoder。 再调用它的 encode 方法,把 person 实例进行编码。 让后整个 JSON 编码操作就完成了。

再来看看如何解析:

let jsonString = "{\"name\ ":\"swift\ ",\"age\ ":22,\"gender\ ":\"female\ "}"
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()

let result = try! decoder.decode(Person.self, from: jsonData)
print(result)  // 输出: Person(name: "swift", gender: "female", age: 22)

解析的时候用的是 JSONDecoder 对象,给他的 decode 方法传入要解析的实例类型 - Person.self, ,再加上要解析的数据对象 jsonData就完成了 JSON 数据的解析。 

使用 Codable 协议就是这么简单, 你不需要些任何具体的解析代码,只需要你的实体类属性名和 JSON 数据能够对应上,就完成了内容的解析。 这样相比 NSJSONSerialization 来看,精简了很多,并且不容易出错。

这里只有一点需要注意,对于我们刚才例子中的 Person 类,除了它自己实现 Codable 协议之外,它的所有属性也必须是遵循 Codable的。 Swift 系统库中的 String,Int,Double,Date,URL,Data 这些类都是实现了 Codable 的。 如果你的自定义属性是其他类型,则需要注意一下它是否也实现了 Codable

另外, 除了 JSONEncoder 和 JSONDecoder 之外, Swift 还为其他类型的数据提供了编解码能力, 比如 PropertyListEncoder 可以编码 plist 数据格式。

对指定属性编码

默认情况下,如果声明继承了 Codable 协议,这个实例中的所有属性都会被算作编码范围内。 如果你只想对一部分属性进行编解码,也是有办法的,可以在你的自定义类中声明一个 CodingKeys 枚举属性:

struct Person : Codable {

   var name: String
   var gender: String = ""
   var age: Int

    enum CodingKeys: String, CodingKey {

        case name
        case age

   }

}

还是之前的 Person 类,这次我们加入了 CodingKeys 属性,并且定义了两个枚举值 name 和 age,只有在 CodingKeys 中指定的属性名才会进行编码,如果我们再次对 Person 进行编码,得到的将会是这样的结果:

{ "name": "swift", "age": 24}

可以看到, gender 属性由于没有在 CodingKeys 中声明,所以不会被编码。 另外如果使用了 CodingKeys,那些没有在 CodingKeys 中声明的属性就必须要要有一个默认值,我们上面的代码中其实给 gender 属性也声明了默认值。

我们还可以使用 CodingKeys 改变编码属性的名称:

struct Person : Codable {

   var name: String
   var gender: String = ""
   var age: Int

    enum CodingKeys: String, CodingKey {

        case name = "title"
        case age

   }

}

还是以 Person 为例,这次我们在 CodingKeys 枚举中讲 name 属性重新定义为 title。 这个意思就是说,虽然在 Person 类中,这个属性名还是 name, 但在编码后的 JSON 中,它的属性名就应该是 title

对上面这个类运行编码后,得到的结果是这样:

{ "title": "swift", "age": 24}

JSON 中的第一个属性名变成了 title, 它对应 Person 类中的 name 属性。

自定义编码过程

你还可以自定义整个编码和解码过程。 对于稍复杂一些的数据结构,这个能力还是会经常用到的。 比如我们想给 Person 再加上身高和体重两个属性:

struct Person : Codable {

   var name: String
   var gender: String = ""
   var age: Int

   var height: Int
   var weight: Int

    enum CodingKeys: String, CodingKey {

        case name = "title"
        case age
        case body

   }

    enum BodyKeys: String, CodingKey {

        case height
        case weight

   }

}

这里面新增的 height 和 width 属性,分别对应体重和身高。 并且还增加了另外一个属性 BodyKeys。 为什么要添加这个属性呢? 是因为我们这次准备把 height 和 width 放到一个单独的对象中。 下面这样解释可能会更直观一些,如果我们不添加 BodyKeys 属性,而是把他们直接定义到 CodingKeys 里面,那么生成的 JSON 结构大致是这样:

{
    "name" : xxx
    "age": xxx
    "height" : xxx
    "weight": xxx
}

但我们单独为 height 和 weight 定义了 BodyKeys 枚举属性。 并且把它有声明到了 CodingKeys 中。 这次 CodingKeys 多了一个 body属性,它对应的就是 BodyKeys 这个枚举。 至于这个对应关系怎么确立的,稍后会讲到。

{
    "name" : xxx
    "age": xxx
    "body": {
        "height" : xxx
        "weight": xxx
   }
}

这样我想应该就说明了 BodyKeys 的作用了。 这样声明完还不行,我们还需要手动的确立他们之间的对应关系,这就要重载 Codable 的两个方法:

extension Person {

    init(from decoder: Decoder) throws {

       let vals = try decoder .container(keyedBy: CodingKeys .self)
       name = try vals .decode(String .self, forKey: CodingKeys .name)
       age = try vals .decode(Int .self, forKey: CodingKeys .age)

       let body = try vals .nestedContainer(keyedBy: BodyKeys .self, forKey: .body)
       height = try body .decode(Int .self, forKey: .height)
       weight = try body .decode(Int .self, forKey: .weight)

   }

   func encode(to encoder: Encoder) throws {

       var container = encoder .container(keyedBy: CodingKeys .self)
       try container .encode(name, forKey: .name)
       try container .encode(age, forKey: .age)

       var body = container .nestedContainer(keyedBy: BodyKeys .self, forKey: .body)
       try body .encode(height, forKey: .height)
       try body .encode(weight, forKey: .weight)

   }

}

init(from decoder: Decoder) 用于解析数据, encode(to encoder: Encoder) 方法用于编码数据。 上面的代码我想不用过多解释,很容易理解。

decoder.container() 方法首先获取 CodingKey 的对应关系,这里我们首先传入 CodingKeys.self 表示我们先前声明的类型。 然后调用 vals.decode() 方法,用于解析某个单独的属性。 接下来调用 vals.nestedContainer() 方法获取内嵌的层级,也就是我们先前声明的 BodyKeys。然后继续解析。

编码的相关处理也大同小异,把上面解码方法中的逻辑反向处理了一遍。

这样,如果我们对新的 Person 实例再进行编码,得到的将会是这样的结果:

{ "title": "swift", "age": 24, "body":{ "weight": 80, "height": 180}}

可以看到,生成了带层级的 JSON 数据。

总结

Codable 协议的设计,可以帮助我们产出更好的代码结构。对于简单的数据模型,不需要任何处理即可使用。 而稍复杂的数据结构,也只需要将解析规则封装到实体类中,可以有效避免代码结构的散乱。

总之,像是数据解析这类的操作,在平时的开发工作中还是比较多的。 如果你正在开发 Swift 项目,它是一个你值得了解的特性。



 
SwiftCafe 更多文章 旅行青蛙,这个时代的有趣产物 程序员在 AI 时代的新机会 原创开源组件 GracefulImagePicker 赠书活动获奖用户 给老朋友们的赠书活动
猜您喜欢 Android 提高代码质量 之 多种检测方案 且说付费 聊聊WannaCry电脑勒索病毒 通过Har生成测试脚本 (LR 为例) Python 编码错误的本质原因