translation icon indicating copy to clipboard operation
translation copied to clipboard

Friday Q&A 2017-07-14: Swift.Codable

Open saitjr opened this issue 6 years ago • 5 comments

https://www.mikeash.com/pyblog/friday-qa-2017-07-14-swiftcodable.html

saitjr avatar Mar 27 '18 14:03 saitjr

title: "Friday Q&A 2017-07-14: Swift.Codable" date: tags: categories: [Mike Ash] permalink: friday-qa-2017-07-14-swift-codable keywords: custom_title: description:


原文链接=https://www.mikeash.com/pyblog/friday-qa-2017-07-14-swiftcodable.html 作者=Mike Ash 原文日期=2017/7/14 译者=冬瓜 校对=numbbbbb 定稿=

Swift 4 中有趣的扩展之一就是 Codable 协议和其组合策略。这也就是我最近十分关注的一个主题,所以今天我们来讲讲它到底是什么,以及它的原理。

序列化 - Serialization

将值类型的对象序列化后存储至磁盘上或是通过网络进行传输,是十分常见的需求,尤其在当前这个无时无刻在线的时代。

迄今为止,Apple 生态系统的序列化选择十分有限:

  1. NSCoding 提供了对复杂对象的自动序列化方案,并且可以与自定义类兼容,但是十分不适合跨平台数据序列化方案,需要手动编写编码和解码方法。
  1. NSPropertyListSerializationNSJSONSerialization 可以实现 Cocoa 中的 NSDictionary/NSString 或属性列表(property list)和 JSON 格式数据互相转换。JSON 适用于服务器的数据交互。一般来说,API 都有很多没有意义的外层值,因此需要编写很多代码来提取有意义的值。这些代码一般只能用于特定结构的数据,很难处理异常数据。
  2. NSXMLParserNSXMLDocument 适用于 XML 格式的数据传输。但是数据的解析和模型转换规则仍旧需要开发者自行实现。
  3. 最后,完全自己实现编码转换。这个过程很有趣,但是工作量巨大且容易出错。

这几种方法写出的代码都具有相同的模式:声明一个 String 类型的属性,命名为 foo,编码并传输,从传输来的数据中获取 foo 的值并解码,转换成 String 类型存储下来,如果失败就抛出异常。接着声明一个 String 类型的属性,命名为 bar......

开发者自然不喜欢这些重复的过程。重复的事情都应该交给计算机来处理。理想情况下,我们的代码应该是这样:

struct Whatever {
   var foo: String
   var bar: String
}

它是可序列化的,并且能保留所有必要的信息。

反射(Reflection)是实现数据序列化和反序列化的常用方法。许多 Objective-C 开发者都编写了自动将 Objective-C 对象和 JSON 相互转换的代码。在 Swift 中,我们可以直接使用 Objective-C Runtime,也可以结合一些黑科技来使用 Swift Mirror(因为 Swift Mirror 不支持修改属性)。

放眼 Apple 生态圈之外的环境,这种语言特性被大多数语言所采纳,但是也导致了很多安全漏洞。(原文中称之为 hilarious security bugs

反射并不是这个问题的最佳解决方案,易出错且易造成安全问题。这种方法很难使用静态类,因此会在运行时而不是编译时出现更多错误。代码执行起来也比较慢,因为无法确定数据类型,需要做很多字符串查找操作。

Swift 使用了编译时代码生成,而不是 runtime 时反射。这说明编译器将承担大量的工作,但编译出的结果效率很高,并且能使用静态类型。

概述

Swift 最新的编码系统用到了一些基础协议。

Encodable 协议用来实现编码。如果遵循这个协议,并且类中所有的属性都是 Encodable 类型的,那么编译器可以自动编码。如果结果不符合你的要求,或需要一些特殊的处理,你也可以自行实现。

Decodable 协议一般都会与 Encodable 搭配使用,用来做解码。和 Encodable 一样,如果所有属性都是 Decodable,编译器会自动生成。

由于 EncodableDecodable 一般会一起使用,Swift 用另外一个协议将它俩结合在一起:

typealias Codable = Decodable & Encodable

这两个协议都很简单,里面只有一个方法:

protocol Encodable {
   func encode(to encoder: Encoder) throws
}

protocol Decodable {
   init(from decoder: Decoder) throws
}

EncoderDecoder 协议用来指定对象如何编码和解码。一般来说你不需要自己实现代码,因为 Codable 已经提供了默认实现。如果你想自定义 Codable 的实现,就需要了解实现细节。这里比较复杂,我们稍后再讲。

最后,CodingKey 协议用来实现编码和解码过程中用到的键。相比直接使用字符串,这个协议添加了一层静态类型检查。协议包含一个 String 值,并且提供了一个可选的 Int 值,支持用位置下标获取值。

protocol CodingKey {
    var stringValue: String { get }
    init?(stringValue: String)

    var intValue: Int? { get }
    public init?(intValue: Int)
}

编码器和解码器

EncoderDecoder 的用法和 NSCoder 类似。对象会收到一个编码器,可以使用它来执行编码和解码。

NSCoder 的 API 十分简单。NSCoder 有一系列方法,例如 encodeObject:forKey: encodeInteger:forKey: 可以对对象进行编码。这些实例对象也可以使用 encodeObject: encodeInteger: 这样的方法对不含键值的变量进行编码。

Swift 中的 API 算是一种间接实现。Encoder 自身没有任何方法可以对变量进行编码,它只是提供容器,具体的编码方法在容器里。具体来说,Encoder 包含一个键编码(keyed coding)容器,一个无键编码(unkeyed encoding)容器,还有一个单值编码(encoding a single value)容器。

梁杰:校对到这里。

这样我们就可以根据需求。NSCoder 只能处理 Apple 的编码格式,所以它会。Encoder 必须通过 JSON 这种格式进行工作。如果一个实例使用的是键值格式来描述属性,则需要使用键值编码方式,继而产生一个 JSON 集合。如果使用无键量对对象进行描述,则应该产生一个 JSON 数组。当对象为空且无编码值那么该如何处理呢?使用 NSCoder,将无法得知输出结果。但是使用 Encoder,对象实例则会相应一个含键或者无键的容器,编码器可以从中自适配。

Decoder 工作方式与其类似。不会直接从值中解码,而是通过容器类型进行解码。同样的,也提供前文的三种容器类型。

由于容器的代码设计,EncoderDecoder 的协议十分轻量。他们中仅声明了一些属性信息和容器获取的方法:

protocol Encoder {
    var codingPath: [CodingKey?] { get }
    public var userInfo: [CodingUserInfoKey : Any] { get }

    func container<Key>(keyedBy type: Key.Type)
        -> KeyedEncodingContainer<Key> where Key : CodingKey
    func unkeyedContainer() -> UnkeyedEncodingContainer
    func singleValueContainer() -> SingleValueEncodingContainer
}

protocol Decoder {
    var codingPath: [CodingKey?] { get }
    var userInfo: [CodingUserInfoKey : Any] { get }

    func container<Key>(keyedBy type: Key.Type) throws
        -> KeyedDecodingContainer<Key> where Key : CodingKey
    func unkeyedContainer() throws -> UnkeyedDecodingContainer
    func singleValueContainer() throws -> SingleValueDecodingContainer
}

复合类型也同样的存在于容器中。可以通过编写 Codable 类型的属性来获得对应的容器,但这之前你需要了解一些可以直接编解码的初始类型。对于 Codable 来说,IntFloatDoubleBoolString 这些类型是可以直接支持的。这些基础类型已经可以解决绝大多数类的编解码码方案了。无键容器也支持以上基础类型的编码序列。

除了这些基本的方法之外,还有一些补充方法用来支持特殊用例。KeyedDecodingContainer 有着 decodeIfPresent 这个方法用来返回一个 Optional 值,代替弃键操作而返回 nil。编码容器具有用于弱引用编码方式,在可编码对象的成员中包含可编码对象这一场景时可以使用(用于处理继承引用的复杂图问题)。另外还有一些获取嵌套容器的方案,这些方法运行对整体的层次进行编码。最后在介绍一种可获取 "super" 编码器或解码器的方法,该场景可以使得子类和超类无冲突共存。子类可以在编码的过程中,请求超类的 "super" 编码器进行编码,确保键无冲突。

Codable 实现

实现 Codable 协议十分容易:保证声明的一致性就可以自动生成。

你需要了解当中发生了什么。我们先来看看它最终的生成结果以及需要编写的代码。下面将从一个示例开始讲述 Codable

struct Person: Codable {
    var name: String
    var age: Int
    var quest: String
}

编译器将会生成嵌套在 Person 中的 CodingKeys 类型。如果来自行编写,这个类型将是这样的:

private enum CodingKeys: CodingKey {
    case name
    case age
    case quest
}

这个示例其名称与 Persion 结构体名称相匹配。编译器会为每一个 CodingKeys 枚举量自动匹配命名,其中的属性名称亦是如此。

如果我们需要不同的名称,可以通过为 CodingKeys 提供自定义命名方案,示例如下:

private enum CodingKeys: String, CodingKey {
    case name = "person_name"
    case age
    case quest
}

这会使得 name 属性以 person_name 的命名进行编解码。编译器会接受我们自定义的 CodingKeys 类型,并同时为 Codable 的剩余部分提供默认实现。这也就是说,我们可以对部分属性进行修改,编译器仍然会工作。

编译器还会生成一个用于 encode(to:)init(from:) 方法的默认实现。encode(to:) 方法的实现中能够获得一个含键容器用于属性编码:

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)
    try container.encode(quest, forKey: .quest)
}

另外编译器会生成一个 init(from:) 方法来保证容器在初始化时的动作:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    name = try container.decode(String.self, forKey: .name)
    age = try container.decode(Int.self, forKey: .age)
    quest = try container.decode(String.self, forKey: .quest)
}

这就是所有的实现。如同 CodingKeys 一样,如果需要定制,可以直接重写实现该方法实现自己的版本。由于这个方法不能对某些属性指定,所以如果需要定制则需要重写整个方法。但是这并不复杂。

手动实现一个适配于 Person 结构体的 Codable 协议如下所示:

extension Person {
    private enum CodingKeys: CodingKey {
        case name
        case age
        case quest
    }

    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)
        try container.encode(quest, forKey: .quest)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        name = try container.decode(String.self, forKey: .name)
        age = try container.decode(Int.self, forKey: .age)
        quest = try container.decode(String.self, forKey: .quest)
    }
}

Encoder 和 Decoder 实现

一般来说不需要自己实现 EncoderDecoder。Swift 已经为我们提供了 JSON 与属性列表的编解码方案,这些已经可以满足我们的日常开发需要。

当然你也可以定制它们,来支持自定义的格式规范。容器协议的实现其实并不难,只是重复性问题。这是一个工程量问题而不是复杂性问题。

如果要实现定制 Encoder,你需要实现 Encoder 协议以及相关容器的协议。三种容器的协议涉及到了很多重复繁琐的编解码工作以及各个类型的变解码方案。

他们如何工作取决于你的代码实现。Encoder 可能会存储正在编码的数据,并告知不同的 Encoder 进行编码处理。

实现 Decoder 也是类似的。需要加上容器协议即可。Decoder 将保存序列化之后的数据,并将请求的结果传递给容器。

我一直在测试一种二进制编码器和解码器来学习这个协议,并希望在之后的文中作为一个例子来讲述如何实现。

总结

Swift 4 的 Codable API 十分可观,可以简化许多代码。典型的 JSON 问题,完全可以交给 Model 来处理,具体的编解码工作交个编译器来完成。特殊情况时,可以实现协议来定制处理方式。

EncoderDecoder 协议较为复杂这是有理的。通过 EncoderDecoder 的定制来实现需要的方法,其中的主要实现都是重复而繁琐的。

Tuccuay avatar May 08 '18 16:05 Tuccuay

这个 @Desgard 已经翻译过了,直接搬运过来的 🌚🌚🌚

https://idevqa.github.io/FridayQA/Swift-Codable

Tuccuay avatar May 08 '18 16:05 Tuccuay

@Tuccuay 可以直接校对下😄

Desgard avatar May 09 '18 00:05 Desgard

质量过差,我重新做下翻译。

Desgard avatar May 11 '18 16:05 Desgard

@Desgard 记得重新翻译啊

numbbbbb avatar Dec 04 '18 02:12 numbbbbb