原文:Understanding type erasure in Swift – Donny Wals
Swift 的类型系统(大部分)非常棒。其严格的约束和灵活的泛型允许开发人员以极其安全的方式表达复杂的概念,因为 Swift 编译器会检测并标记程序中类型的任何不一致之处。
虽然这在大多数时候都很好,但有时 Swift 的严格类型化会妨碍我们试图建立的东西。如果你正在编写涉及协议和泛型的代码,这一点尤为重要。
有了协议和泛型,你可以表达出复杂到极致的想法,而且很灵活。但有时你正在愉快地编码,Swift 编译器却开始对你大喊大叫。你遇到了这样的情况:你的代码是如此的灵活和动态,而 Swift 却不接受。
例如,你想写一个函数,返回一个遵守协议的对象,而这个协议有一个关联类型。除非你使用一个不透明结果类型(opaque result type),否则是不可能的。
但如果你不想一直从你的函数中返回完全相同的具体类型呢?不幸的是,不透明结果类型在这方面帮不了你。
当 Swift 编译器一直对你大喊大叫,而你却不知道如何让它停下来时,可能是时候应用一些类型擦除了。
在本周的博文中,我将解释什么是类型擦除(type erasure),并展示一个例子,说明如何使用类型擦除来编写高度灵活的代码,让 Swift 编译器正常工作。
有多种情况下,类型擦除是有意义的,我想介绍其中的两种情况。
使用类型擦除来隐藏实现细节
思考类型擦除的最直接方式是将其视为一种隐藏对象的 "真实" 类型的方式。我马上想到的一些例子是 Combine 的 AnyCancellable 和 AnyPublisher。Combine 中的 AnyPublisher 是对输出和失败的泛称。如果你对 Combine 不熟悉,你可以在本博客的 Combine 分类中阅读。关于 AnyPublisher,你真正需要知道的是,它符合 Publisher 协议,并包装了另一个发布器。Combine 有大量的内置发布器,如 Publishers.Map、Publishers.FlatMap、Future、Publishers.Filter,还有很多很多。
通常,当你使用 Combine 时,你会写一些函数来建立一个发布器链。你通常不希望把你使用的发布者暴露给你的函数的调用者。从本质上讲,你想暴露的是,你正在创建一个发布器,它可以发出某种类型的值(Output),或者以某种特定的错误(Failure)失败。因此,不要写这个:
func fetchData() -> URLSession.DataTaskPublisher<(data: Data, response: URLResponse), URLError> {
return URLSession.shared.dataTaskPublisher(for: someURL)
}
你通常会想这样写:
func fetchData() -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
return URLSession.shared.dataTaskPublisher(for: someURL)
.eraseToAnyPublisher()
}
通过对在 fetchData 中创建的发布器应用类型擦除,我们现在可以根据需要自由地改变它的实现,fetchData 的调用者不需要关心引擎盖下使用的确切的发布器。
当你考虑如何重构这段代码时,你可能会被诱惑去尝试使用协议而不是 AnyPublisher。你会想为什么我们不这样做,这是对的。
由于 Publisher 有一个我们希望能够使用的 Output 和 Failure,使用 some Publisher 是行不通的。由于 Publisher 的关联类型限制,我们将无法返回 Publisher,所以返回 some Publisher 将允许代码编译,但这将是非常无用的:
func fetchData() -> some Publisher {
return URLSession.shared.dataTaskPublisher(for: someURL)
}
fetchData().sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { output in
print(output.data) // Value of type '(some Publisher).Output' has no member 'data'
})
因为 some Publisher 隐藏了 Publisher 所使用的泛型的真实类型,所以在这个例子中,没有办法对输出或完成做任何有用的事情。AnyPublisher 就像 some Publisher 那样隐藏了底层类型,只是你仍然可以通过编写 AnyPublisher<Output, Failure> 来定义 Publisher 的输出和失败类型。
我将在下一节向你展示类型擦除是如何工作的。但首先我想向你展示 Combine 框架中类型擦除的一个稍微不同的应用。在 Combine 中,你会发现一个叫做 AnyCancellable 的对象。如果你使用 Combine,当你使用 Combine 内置的订阅方法订阅发布者时,你将遇到 AnyCancellable。
不用说得太详细,Combine 有一个叫做 Cancellable 的协议。这个协议要求符合该协议的对象实现一个取消方法,可以被调用来取消对发布者输出的订阅。Combine 提供了三个符合 Cancellable 的对象。
AnyCancellableSubscribers.AssignSubscribers.Sink
Assign 和 Sink 订阅者与 Publisher 的两个方法相匹配:
assign(to:on:)sink(receiveCompletion:receiveValue)
这两个方法都返回 AnyCancellable 实例,而不是 Subscribers.Assign 和 Subscribers.Sink。苹果可以选择让这两个方法返回 Cancellable 而不是 AnyCancellable。
但他们没有这样做。
。。。