Swift 进阶 - 编码和解码

Swift 中有如数字、字符串、数组、字典和集合等内置的数据结构,当需要和其他数据结构如 JSON 和 PropertyList,互相进行转换时,就会涉及到编码和解码。
编码和解码的协议
protocol Encodable {
// 把值编码到 encoder 中
func encode(to encoder: Encoder) throws
}
protocol Decodable {
// 从 decoder 中把值解码出来
init(from decoder: Decoder) throws
}
typealias Codable = Decodable & Encodable
JSON 编码和解码
满足 Codable 协议
如下代码中,GroceryProduct 只包含基础数据类型,所以编译器可以自动生成满足 Codable 协议的代码:
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
JSONEncoder
let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(pear)
print(String(data: data, encoding: .utf8)!)
/* Prints:
{
"name" : "Pear",
"points" : 250,
"description" : "A ripe pear."
}
*/
JSONDecoder
let json = """
{
"name": "Durian",
"points": 600,
"description": "A fruit with a distinctive scent."
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let product = try decoder.decode(GroceryProduct.self, from: json)
print(product.name) // Prints "Durian"
编码过程详解
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
}
struct Placemark: Codable {
var name: String
var coordinate: Coordinate
}
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) // 👈 pay attention
} catch {
print(error.localizedDescription)
}
如上代码中,encode 这一步会转换成如下代码,places 是 Array,会调用 Array 对于 encode(to:) 的实现:
places.encode(to: self)
Array 对于 Encodable 协议的实现大概如下,先得到 UnkeyedEncodingContainer 容器,然后遍历 Array 中的元素,将其装到容器中,UnkeyedEncodingContainer 容器中 encode 的实现会判断 Array 中的元素,如果是基础数据类型,会用 SingleValueEncodingContainer 容器将数据装起来,如果不是基础数据类型时,会继续调用其 encode(to:) 方法,就这样层层转换下去,直到基础数据类型:
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)
}
}
}
上面简单说了 SingleValueEncodingContainer 容器,Encoder 协议中共有如下 3 种容器:
- KeyedEncodingContainer 类似于字典,键值对容器,键值是强类型。
- UnkeyedEncodingContainer 类似于数组,连续值容器,没有键值。
- SingleValueEncodingContainer 基础数据类型容器。
protocol Encoder {
container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key>
unkeyedContainer() -> UnkeyedEncodingContainer
singleValueContainer() -> SingleValueEncodingContainer
}
可以得出,整个编码的过程是将 Swift 数据结构层层转换为容器,容器转换为其他数据结构如 JSON 和 PropertyList。
满足 Codable 协议的代码生成
编译器可以自动生成满足 Codable 协议的代码:
生成 Coding Keys
struct Placemark {
private enum CodingKeys: CodingKey {
case name
case coordinate
}
}
生成 encode(to:)
可以看到如下代码中,使用的是 KeyedEncodingContainer 容器:
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)
}
}
生成 init(from:)
这里是关于解码的代码,从容器中获取 Swift 数据结构:
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)
}
}
同编码一样,解码相关的 Decoder 协议也有如下 3 个容器:
protocol Decoder {
container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key>
unkeyedContainer() throws -> UnkeyedDecodingContainer
singleValueContainer() throws -> SingleValueDecodingContainer
}
自己实现 Codable 协议
根据实际的需求,有时候需要自己实现或部分实现 Codable 协议。
自定义 Coding Keys
enum CodingKeys 中 case 就是需要编解码,不再其中就不需要编解码,还可以修改转换时对应的键值,可以看到如果只修改其中几项,全部都要写一遍,非常的冗长。
class ItemModel: Codable {
var id: String?
var fullName: String?
var shortName: String?
var description: String?
var displayDescription: String?
var shareDescription: String?
var activityItemNo: String?
var activityUUID: String?
var listPrice: String?
var skus: [SkuModel]?
var inStockSkus: [SkuModel] = []
var images: [String]?
var mainImageIndex: String?
var itemMedias: [String]?
var unfold = false
var increasePrice: Decimal?
var increaseType: ShareIncreaseType?
var isShared = false
var reloading = false
var hasCustomService = false
var memberLevel: MemberLevelType?
var nextMemberLevel: MemberLevelType?
var medias: [String]?
enum CodingKeys: String, CodingKey {
case id
case fullName
case shortName
case description
case displayDescription
case shareDescription
case activityItemNo
case activityUUID = "activityUuid"
case listPrice
case skus
case images
case itemMedias
case mainImageIndex
case medias
}
}
自定义 init(from:)
当要处理如下不标准 JSON 数据时,coordinate 不存在时返回的是空 JSON 对象:
let invalidJSONInput = """
[
{
"name" : "Berlin",
"coordinate": {}
}
]
"""
如下代码捕获了 coordinate 解码不成功的异常,将 coordinate 赋值为 nil:
struct Placemark: Codable {
var name: String
var coordinate: Coordinate
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 和 PropertyList 的来源方有足够的掌控,数据交换就可以按照数据格式语义来,不会有什么问题,如果没有足够的掌控,也许就需要做很多上面的自定义工作,就比较麻烦。