[译] 在 Codable 中实践类型推断

原文: Type inference-powered serialization in Swift
作者推特:@johnsundell

类型推断是 Swift 类型系统的一个重要特性,在语言的语法层面扮演着重要的角色。如果编译器可以从上下文中推测出类型,就可以省掉手写类型的声明,减少啰嗦的代码。
类型推断美妙的地方在于很多时候你甚至都不会注意到它的存在,即便它几乎作用于每行你写的代码中。如果我们设计 API 的时候充分利用类型推断(尤其在使用了泛型的时候),我们可以减少啰嗦的代码和代码中的“噪音”。
本文将通过利用类型推断改进 Swift Codable 的 API,让它使用起来更简便。

Decoding

给没有了解过 Codable 的朋友先简单介绍一下,它是 Swift 自带的用于编码、解码二进制数据的一组 API。常用于 JSON 解析,会在编译器层面自动合成一些模板代码代替手写序列化代码。
Codable 是 Decodable 和 Encodable 两个协议的别名(Decodable & Encodable),分别表示一个类型可以被解码和编码。
首先展示一下如何在从 JSON 中解析 Decodable 时利用类型推断。下面的代码使用默认的方式把 data 解析数据成 User 的实例,最后把这个 user 实例传给登录模块,通知系统一个用户登录了:

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
userDidLogin(user)

上面的代码没有什么问题,但是我们可以加上类型推断来做一点改进。首先我们给 Data 增加一个 extension 方法,可以通过调用 decoded()直接解码出任意的 Decodable 类型:

extension Data {
    func decoded<T: Decodable>() throws -> T {
        return try JSONDecoder().decode(T.self, from: self)
    }
}

我们去掉了要解析的类型参数,把类型的获取判断交给了编译器去推断。我们所要做的就是告诉它一个特定的值应该被当作什么类型,然后编译器就会处理剩下的工作。这样改动后我们就可以用一个更加的优雅的方式实现了:

let user = try data.decoded() as User

我们现在已经引入了类型推断,如果通过上下文可以知道类型,那么我们完全可以不用单独声明一个类型。也就是说我们可以把前面第一个例子解析的三行代码减少到只要一行。因为 userDidLogin 接收一个 User 类型的参数,所以类型系统可以知道需要解析的类型:

try userDidLogin(data.decoded())

真香! 🍭

提高灵活性

加上类型推断的 API 使用起来已经很方便了,不过我还可以做一些改动让它有更好的灵活性。首先我们现在都是在内部创建一个 JSONDecoder,大部分时候也是使用 JSONDecoder。可以通过默认参数来优化,并且这样也可以很好的兼容其他的 Decoder:

extension Data {
    func decoded<T: Decodable>(using decoder: JSONDecoder = .init()) throws -> T {
        return try decoder.decode(T.self, from: self)
    }
}

依赖参数化不仅可以提高灵活性,如果你使用了依赖注入,同样也可以提高可测试性。关于依赖注入可以查看这篇 Different flavors of dependency injection in Swift
现在我们可以在 API 中使用任何一个 JSONDecoder,但是这样会和解析 JSON 耦合在一起。实际上有一些 app 也会使用其他的序列化格式,比如系统也支持的 Property Lists。我们应该有一种方式可以支持其他类型的序列发。
好消息是我们可以通过声明一个 protocol 来完成这件事,坏消息是 swift 里没有这样一个现成的 protocol。我们可以根据现有的 decoder 方法抽象出一个 protocol,然后再让需要使用到的 decoder 实现这个接口。比如这样:

protocol AnyDecoder {
    func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
}

extension JSONDecoder: AnyDecoder {}
extension PropertyListDecoder: AnyDecoder {}

接着我们再把 Data extension 里的方法修改一下,使用我们前面定义的 AnyDecoder,使用 JSONDecoder 作为默认参数:

extension Data {
    func decoded<T: Decodable>(using decoder: AnyDecoder = JSONDecoder()) throws -> T {
        return try decoder.decode(T.self, from: self)
    }
}

现在这样使用起来既方便又灵活。😀

Encoding

对于 encoding 我们也可以做类似的操作。我们创建一个类似的 protocol,让 JSONEncoder、PropertyListEncoder 实现这个接口:

protocol AnyEncoder {
    func encode<T: Encodable>(_ value: T) throws -> Data
}

extension JSONEncoder: AnyEncoder {}
extension PropertyListEncoder: AnyEncoder {}

接着给 Encodable 添加使用接口的扩展方法,使用 JSONEncoder 作为默认参数:
```swift
extension Encodable {
func encoded(using encoder: AnyEncoder = JSONEncoder()) throws -> Data {

top Created with Sketch.