编码和解码
一个类型通过声明自己遵守 Encodable
和/或 Decodable
协议,来表明可以被序列化和/或反序列化。这两个协议都只约束了一个方法,其中:Encodable
约束了 encoda(to:)
,它定义了一个类型如何对自身进行编码;而 Decodable
则约束了一个初始化方法,用来从序列化的数据中创建实例:
/// 一个类型可以将自身编码为某种外部表示形式。
public protocol Encodable {
/// 将值编码到给定的 encoder 中。
public func encode(to encoder: Encoder) throws
}
/// 一个类型可以从某种外部表示形式中解码得到自身。
public protocol Decodable {
/// 从给定的 decoder 中解码来创建新的实例。
public init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable
一个最小的例子
自动遵循协议
下面的 Coordinate
存储了一个GPS位置信息:
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
// 不需要实现
}
struct Placemark: Codable {
var name: String
var coordinate: Coordinate
}
Encoding
Swift 自带两个编码器,分别是 JSONEncoder
和 PropertyListEncoder
(它们定义在 Foundation
中,而不是在标准库里)。
let places = [
Placemark(name: "Berlin", coordinate:
Coordinate(latitude: 52, longitude: 13))
Placemark(name: "Cape Town", coordinate:
Coordinate(latitude: -34, longitude: 18))
]
do {
let encoder = JSONEncoder()
let jsonData = try encoder.encode(places) // 129 bytes
let jsonString = String(decoding: jsonData, as: UTF8.self)
/*
[{"name":"Berlin",
"coordinate":{"longitude":13,"latitude":52}},
{"name":"Cape Town",
"coordinate":{"longitude":18,"latitude":-34}}]
*/
} catch {
print(error.localizedDescription)
}
Decoding
JSONEncoder 的解码器版本是 JSONDecoder。
do {
let decoder = JSONDecoder()
let decoded = try decoder.decode([Placemark].self, from: jsonData)
// [Berlin (lat:52.0, lon:13.0), Cap Town (lat: -34.0, lon: 18.0)]
type(of: decoded) // Array<Placemark>
decoded == places // true
} catch {
print(error.localizedDescription)
}
编码过程
容器
来看看 Encoder
协议,这是编码器暴露给编码值的接口:
/// 一个可以把值编码成某种外部表现形式的类型。
public protocol Encoder {
/// 编码到当前位置的编码键 (coding key) 路径
var codingPath: [CodingKey] { get }
/// 用户为编码设置的上下文信息。
var userInfo: [CodingUserInfoKey: Any] { get }
/// 返回一个容器,用于存放多个由给定键索引的值。
func container<Key: CodingKey>(keyedBy type: Key.Type)
-> KeyedEncodingContainer<Key>
/// 返回一个容器,用于存放多个没有键索引的值。
func unkeyedContainer() -> UnkeyedEncodingContainer
/// 返回一个适合存放单一值的编码容器。
func singleValueContainer() -> SingleValueEncodingContainer
}
先忽略 codingPath
和 userInfo
,显然 Encoder
的核心功能就是提供一个 编码容器 (encoding container)。一个容器就是编码器内部存储的一种沙盒视图。通过为每个要编码的值创建一个新的容器,编码器能够确保每个值都不会覆盖彼此的数据。
容器有三种类型:
- 键容器 (Keyed Container) 用于编码键值对。可以把键容器想象为一个特殊的字典,这是到目前为止,应用最普遍的容器。
- 无键容器 (Unkeyed Container) 用于编码一系列值,但不需要对应的键,可以将它想象成保存编码结果的数组。
- 单值容器 对单一值进行编码。
对于这三种容器,它们每个都对应了一个协议,来约束容器应该如何接受一个值并进行编码。下面是 SingleValueEncodingContainer
的定义:
/// 支持存储和直接编码无索引单一值的容器。
public protocol SingleValueEncodingContainer {
/// 编码到当前位置的编码键路径。
var codingPath: [CodingKey] { get }
/// 编码空值。
mutating func encodeNil() throws
/// 编码原始类型的方法
mutating func encode(_ value: Bool) throws
mutating func encode(_ value: Int) throws
mutating func encode(_ value: Int8) throws
mutating func encode(_ value: Int16) throws
mutating func encode(_ value: Int32) throws
mutating func encode(_ value: Int64) throws
mutating func encode(_ value: UInt) throws
mutating func encode(_ value: UInt8) throws
mutating func encode(_ value: UInt16) throws
mutating func encode(_ value: UInt32) throws
mutating func encode(_ value: UInt64) throws
mutating func encode(_ value: Float) throws
mutating func encode(_ value: Double) throws
mutating func encode(_ value: String) throws
mutating func encode(T: Encodable)(_ value: T) throws
}
UnkeyedEncodingContainer
和 KeyedEncodingContainerProtocol
拥有和 SingleValueEncodingContainer
相同的结构,不过它们具备更多的能力,比如可以创建嵌套的容器。如果你想要为其他数据格式创建编码器或解码器,那么最重要的部分就是实现这些容器。
值是如何对自己编码的
回到之前的例子,我们要编码的顶层类型是 Array<Placemark>
。而无键容器是保存数组编码结果的绝佳场所 (因为数组说白了就是一串值的序列)。因此,数组将会向编码器请求一个无键容器。然后,对自身的元素进行迭代,并告诉容器对这些元素一一进行编码。
extension Array: Encodable where Element: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for element in self {
try container.encode(element)
}
}
}
合成的代码
Coding Keys
首先,在 Placemark
里,编译器会生成一个叫做 CodingKeys
的私有枚举类型:
struct Placemark {
// ...
private enum CodingKeys: CodingKey {
case name
case coordinate
}
}
这个枚举包含的成员与结构体中的存储属性一一对应。而枚举值即为键容器编码对象时使用的键。和字符串形式的键相比,因为编译器会检查拼写错误,所以这些强类型的键更加安全和方便。不过,编码器最后为了存储需要,还是必须要能将这些键转为字符串或者整数值。而完成这个转换任务的,就是 CodingKey
协议:
/// 该类型作为编码和解码时使用的键
public protocol CodingKey {
/// 在一个命名集合 (例如:以字符串作为键的字典) 中的字符串值。
var stringValue: String { get }
/// 在一个整数索引的集合 (一个整数作为键的字典) 中使用的值。
var intValue:Int? { get }
init?(stringValue: String)
init?(intValue: Int)
}
encode(to:) 方法
下面是编译器为 Placemark
结构体生成的 encode(to:)
方法:
struct Placemark: Codable {
// ...
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(coordinate, forKey: .coordinate)
}
}
和编码 Placemark
数组时的主要区别是,Placemark
会将自己编码到一个键容器中。对于那些拥有多个属性的复合数据类型 (例如结构体和类),使用键容器是正确的选择 (这里有一个例外,就是 Range
,它使用无键容器来编码上下边界)。
编码过程的结果,最终是一棵嵌套的容器树。JSON
编码器可以根据树中节点的类型把这个结果转换成对应的目标格式:键容器会变成 JSON
对象 ({ ... }),无键容器变成 JSON
数组 ([]),单值容器则按照它们的数据类型,被转换为数字,布尔值,字符串或者 null。
init(from:) 初始化方法
当我们调用 try decoder.decode([Placemark].self, from: jsonDate)
时,解码器会按照我们传入的类型,使用 Decodable
中定义的初始化方法创建一个该类型的实例。和编码器类似,解码器也管理一棵由 解码容器 (decoding containers)
构成的树,树中所包含的容器我们已经很熟悉了,它们还是键容器,无键容器,以及单值容器。
每个被解码的值会以递归方式向下访问容器的层级,并且使用从容器中解码出来的值初始化对应的属性。如果某个步骤发生了错误 (比如由于类型不匹配或者值不存在),那么整个过程都会失败,并抛出错误。
struct Placemark: Codable {
// ...
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Codingkeys.self)
name = try container.decode(String.self, forKey: .name)
coordinate = try container.decode(Coordinate.self, forKey: .coordinate)
}
}
手动遵守协议
自定义 Coding Keys
控制一个类型如何编码自己最简单的方式,是为它创建自定义的 CodingKeys
枚举。它可以让我们用一种简单的,声明式的方法,改变类型的编码方式。在这个枚举中,我们可以:
- 在编码后的输出中,用明确指定的字符串值重命名字段。
- 将某个键从枚举中移除,以此跳过与之对应字段。
想要设置一个不同的名字,我们需要明确将枚举的底层类型设置为 String
。
struct Placemark2: Codable {
var name: String
var coordinate: Coordinate
private enum CodingKeys: String, CodingKey {
case name = "label"
case coordinate
}
// 编译器合成的 encode 和 decode 方法将使用覆盖后的 CodinhKeys。
}
在下面的实现中,枚举里没有包含 name 键,因此编码时地图标记的名字将会被跳过,只有 GPS 坐标信息会被编码:
struct Placemark3: Codable {
var name: String = "(Unknown)"
var coordinate: Coordinate
private enum CodingKeys: CodingKey {
case coordinate
}
}
自定义的 encode(to:) 和 init(from:) 实现
struct Placemark4: Codable {
var name: String
var coordinate: Coordinate?
}
假设这样的一个 JSON:
let invalidJSONInput = """
[
{
"name": "Berlin",
"coordinate": {}
}
]
"""
因为 coordinate
中并不存在 latitude
和 longitude
字段,所以会触发 .keyNotFound
错误:
do {
let inputData = invalidJSONInput.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try decoder.decode([Placemark4].self, from: inputData)
} catch {
print(error.localizedDescription)
// The data couldn't be read because it is missing.
}
我们可以重载 Decodable
的初始化方法,明确地捕获我们所期待的错误:
struct Placemark4: Codable {
var name: String
var coordinate: Coordinate?
// encode(to:) 依然由编译器合成
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
do {
self.coordinate = try container.decodeIfPresent(Coordinate.self,
forKey: .coordinate)
} catch DecodingError.keyNotFound {
self.coordinate = nil
}
}
}
现在,解码器就可以成功地解码这个错误的 JSON 了:
do {
let inputData = invalidJSONInput.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try decoder.decode([Placemark4].self, from: inputData)
decoded // [Berlin(nil)]
} catch {
print(error.localizedDescription)
}
常见的编码任务
让其他人的代码满足 Codable
import CoreLocation
struct Placemark5: Codable {
var name: String
var coordinate: CLLocationCoordinate2D
}
// 错误:无法自动合成'Decodable'/'Encodable'的适配代码,
// 因为'CLLocationCoordinate2D'不遵守相关协议
extension CLLocationCoordinate2D: Codable {}
// 错误:不能在类型定义的文件之外通过扩展自动合成实现'Encodable'的代码。
extension Placemark5 {
private enum CodingKeys: String, CodingKey {
case name
case latitude = "lat"
case longitude = "lon"
}
func encode(to encoder: Encoder) throws {
var container = encoder.contaienr(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
// 分别编码纬度和经度
try container.encode(coordinate.latitude, forKey: .latitude)
try container.encode(coordinate.longitude, forKey: .longitude)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// 从纬度和经度重新构建 CLLocationCoordinate2D
self.coordinate = CLLocationCoordinate2D(
latitude: try container.decode(Double.self, forKey: .latitude),
longitude: try container.decode(Double.self, forkey: .longitude)
)
}
}
另一种方案是使用嵌套容器来编码经纬度。KeyedDecodingContainer
有一个叫做nestedContainer(keyedBy:forKey:)
的方法,它可以在forKey
指定的键上,新建一个嵌套的键容器,这个嵌套键容器使用 keyedBy
参数指定的另一套编码键。于是,我们只要再定义一个实现了 CodingKeys
的枚举,用它作为键,在嵌套的键容器中编码纬度和经度就好了:
struct Placemark6: Encodable {
var name: String
var coordinate: CLLocationCoordinate2D
private enum CodingKeys: CodingKey {
case name
case coordinate
}
// 嵌套容器的编码键
private enum CoordinateCodingKeys: CodingKey {
case latitude
case longitude
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var coordinateContainer = container.nestedContainer(
keyedBy: CoordinatedCodingKeys.self, forKey: .coordinate)
try coordinateContainer.encode(coordinate.latitude, forKey: .latitude)
try coordinateContainer.encode(coordinate.longitude, forKey: .longitude)
}
}
这样,我们就在 Placemark
结果体里,有效地重建了 Coordinate
类型的编码方式,而没有向 Codable
系统暴露这个内嵌的类型。
另一种策略,在 PlaceMark
里,我们定义一个 Coordinate
类型的私有属性 _coordinate
,用它存储位置信息。然后,给用户暴露一个 CLLocationCoordinate2D
类型的计算属性 coordinate
。
struct Placemark7: Codable {
var name: String
private var _coordinate: Coordinate
var coordinate: CLLocationCoordinate2D {
get {
return CLLocationCoordinate2D(latitude: _coordinate.latitude,
longitude: _coordinate.longitude)
}
set {
_coordinate = Coordinate(latitude: newValue.latitude,
longitude: newValue.longitude)
}
}
private enum CodingKeys: String, CodingKey {
case name
case _coordinate = "coordinate"
}
}
让类满足 Codable
我们不能为一个非 final
的类用扩展的方式事后追加 Codable
特性。推荐的方式是写一个结构体来封装类,并且对这个结构体进行编解码。
让枚举满足 Codable
编译器也可以为实现了 RawRepresentable
协议的枚举自动合成实现 Codable
的代码,只要枚举的 RawValue
类型是这些原生就支持 Codable
的类型即可:Bool,String,Float,Double 以及各种形式的整数。而对于其他情况,例如带有关联值的枚举,就只能手动添加 Codable
实现了。
Either 是一个非常常用的表达二选一概念的类型,它表示的值既可以是泛型参数 A 的对象,也可以是泛型参数 B 的对象:
enum Either<A,B> {
case left(A)
case right(B)
}
extension Either: Codable where A: Codable, B: Codable {
private enum CodingKeys: CodingKey {
case left
case right
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .left(let value):
try container.encode(value, forKey: .left)
case .right(let value):
try container.encode(value, forKey: .right)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let leftValue = try container.decodeIfPresent(A.self, forKey: .left) {
self = .left(leftValue)
} else {
let rightValue = try container.decode(B.self, forKey: .right)
self = .right(rightValue)
}
}
}
let values: [Either<String,Int>] = [
.left("Forty-two"),
.right(42)
]
do {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let xmlData = try encoder.encode(values)
let xmlString = String(decoding: xmlData, as: UTF8.self)
/*
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>left</key>
<string>Forty-two</string>
</dict>
<dict>
<key>right</key>
<integer>42</integer>
</dict>
</array>
</plist>
*/
let decoder = PropertyListDecoder()
let decoded = try decoder.decode([Either<String, Int>].self, from: xmlData)
/*
[Either<Swift.String, Swift.Int>.left("Forty-two"),
Either<Swift.String, Swift.Int>.right(42)]
*/
} catch {
print(error.localizedDescription)
}
解码多态集合
解码器要求我们为要解码的值传入具体的类型。直觉上这很合理:解码器需要知道具体的类型才能调用合适的初始化方法,而且由于被编码的数据一般不含有类型信息,所以类型必须由调用者来提供。这种对强类型的强调导致了一个结果,那就是在解码步骤中不存在多态的可能。
假设我们想编码这样一个 UIView
的数组。(假定 UIView 和它的子类现在都满足 Codable,当然现在它们并不是这样的类型。)
let views: [UIView] = [label, imageView, button]
如果我们编码这个数组,在对它进行解码,解码器能还原回来的只是普通的 UIView 对象,因为它对被解码数据类型的全部了解就是 [UIView].self。
编码这样的多态对象集合,最好的方式就是创建一个枚举,让它的每个 case 对应我们要支持的子类,而 case 的关联值则是对应的子类对象:
enum View {
case view(UIView)
case label(UILabel)
case imageView(UIImageView)
// ...
}