什么是Type-Erasure
swift中在引用一些通用协议时,往往需要用到type erasure
。它使我们能够更轻松地与通用协议进行交互,这些通用协议对将要实现它们的各种类型具有特定的要求。
常见的:
struct ContentView: View {
var body: AnyView {
AnyView(Text("Hello, world!"))
}
}
// 当然,5.1之后有更简单的写法(Opaque return types)
struct ContentView: View {
var body: some View {
Text("Hello, world!")
}
}
实例
以Equatable标准库中的协议为例。由于都是为了使相等类型的两个值能够按相等性进行比较,因此它使用元Self类型作为其唯一方法要求的参数
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
这种方法的优点在于,它不可能意外地比较两个不相关的相等类型(例如User和String),但是,它也使得它不可能被引用Equatable为独立协议(例如创建类似的数组[Equatable]),因为为了能够使用它,编译器需要知道确切的确切类型实际上符合该协议。
当协议包含关联类型时,也是如此。例如,在这里我们定义了一个Request协议,使我们能够在单个统一的实现后隐藏各种形式的数据请求(例如网络调用,数据库查询和缓存提取):
protocol Request {
associatedtype Response
associatedtype Error: Swift.Error
typealias Handler = (Result<Response, Error>) -> Void
func perform(then handler: @escaping Handler)
}
上面的方法为我们提供了相同的权衡方法,Equatable它非常强大,因为它使我们能够为任何类型的请求创建通用的抽象,但是这也使得无法直接引用Request协议本身,如下所示:
class RequestQueue {
// Error: protocol 'Request' can only be used as a generic
// constraint because it has Self or associated type requirements
func add(_ request: Request,
handler: @escaping Request.Handler) {
...
}
}
解决上述问题的一种方法是准确执行错误消息中所说的内容,而不是Request直接引用,而是将其用作一般约束:
class RequestQueue {
func add<R: Request>(_ request: R,
handler: @escaping R.Handler) {
...
}
}
上面的方法起作用了,因为现在编译器能够保证传递handler的确实与Request传递的实现兼容request—因为它们都基于泛型R,而泛型又被限制为与一致Request。
但是,尽管我们解决了方法签名问题,但实际上仍然无法对传递的请求做很多工作,因为我们无法将其存储为Request属性或[Request]数组的一部分,这将使得继续建立RequestQueue。那么,type-erasure该出场了。
通用包装器类型
我们将探讨的第一种类型擦除实际上并不涉及擦除任何类型,而是将它们包装在一个我们可以更容易引用的泛型类型中。继续构建RequestQueue,我们将首先创建该包装器类型,该包装器类型将捕获每个请求的perform方法作为闭包,以及在请求完成后应调用的handler方法:
// This will let us wrap a Request protocol implementation in a
// generic has the same Response and Error types as the protocol.
struct AnyRequest<Response, Error: Swift.Error> {
typealias Handler = (Result<Response, Error>) -> Void
let perform: (@escaping Handler) -> Void
let handler: Handler
}
接下来,实现RequestQueue,为其添加Response和Error类型的泛型,这样编译器可以将泛型类型和associatetype对齐,从而允许我们对Request存储和引用。
这个实现时线程不安全的,我们只用来说明类型抹除的问题
class RequestQueue<Response, Error: Swift.Error> {
private typealias TypeErasedRequest = AnyRequest<Response, Error>
private var queue = [TypeErasedRequest]()
private var ongoing: TypeErasedRequest?
// We modify our 'add' method to include a 'where' clause that
// gives us a guarantee that the passed request's associated
// types match our queue's generic types.
func add<R: Request>(
_ request: R,
handler: @escaping R.Handler
) where R.Response == Response, R.Error == Error {
// To perform our type erasure, we simply create an instance
// of 'AnyRequest' and pass it the underlying request's
// 'perform' method as a closure, along with the handler.
let typeErased = AnyRequest(
perform: request.perform,
handler: handler
)
// Since we're implementing a queue, we don't want to perform
// two requests at once, but rather save the request for
// later in case there's already an ongoing one.
guard ongoing == nil else {
queue.append(typeErased)
return
}
perform(typeErased)
}
private func perform(_ request: TypeErasedRequest) {
ongoing = request
request.perform { [weak self] result in
request.handler(result)
self?.ongoing = nil
// Perform the next request if the queue isn't empty
...
}
}
}
闭包实现
使用闭包擦除类型时,其思想是捕获在闭包内部执行操作所需的所有类型信息,并使闭包仅接受非通用(甚至Void)输入。这样一来,我们就可以引用,存储和传递该功能,而无需实际知道其内部发生了什么—从而为我们提供了更大的灵活性。当然,这样实现会让代码变得难以调试,但优点是方便,而且可以完全封装类型信息。
class RequestQueue {
private var queue = [() -> Void]()
private var isPerformingRequest = false
func add<R: Request>(_ request: R,
handler: @escaping R.Handler) {
// This closure will capture both the request and its
// handler, without exposing any of that type information
// outside of it, providing full type erasure.
let typeErased = {
request.perform { [weak self] result in
handler(result)
self?.isPerformingRequest = false
self?.performNextIfNeeded()
}
}
queue.append(typeErased)
performNextIfNeeded()
}
private func performNextIfNeeded() {
guard !isPerformingRequest && !queue.isEmpty else {
return
}
isPerformingRequest = true
let closure = queue.removeFirst()
closure()
}
}
外部类型抹除
目前,我们都是在内部实现类型抹除,外部调用者不会关心到这个问题。但是,有时在将协议实现传递给API之前进行一些轻量级转换,可以简化更多工作,而且可以更整齐的封装抹除代码。
对我们这个例子来说,可以要求每一个request在添加到队列之前进行专门化管理,这就衍变出了RequestOperation
struct RequestOperation {
fileprivate let closure: (@escaping () -> Void) -> Void
func perform(then handler: @escaping () -> Void) {
closure(handler)
}
}
类似于闭包实现,我们将其放在extension中
extension Request {
func makeOperation(with handler: @escaping Handler) -> RequestOperation {
return RequestOperation { finisher in
// We actually want to capture 'self' here, since otherwise
// we risk not retaining the underlying request anywhere.
self.perform { result in
handler(result)
finisher()
}
}
}
}
那么,我们的RequestQueue就可以更加专注于队列的实现,不用关心类型擦除了:
class RequestQueue {
private var queue = [RequestOperation]()
private var ongoing: RequestOperation?
// Since the type erasure now happens before a request is
// passed to the queue, it can simply accept a concrete
// instance of 'RequestOperation'.
func add(_ operation: RequestOperation) {
guard ongoing == nil else {
queue.append(operation)
return
}
perform(operation)
}
private func perform(_ operation: RequestOperation) {
ongoing = operation
operation.perform { [weak self] in
self?.ongoing = nil
// Perform the next request if the queue isn't empty
...
}
}
}
当然,在调用RequestQueue时,就要求我们手动将Request转换为RequestOption了。
文档信息
- 本文作者:Yawei Wang
- 本文链接:https://pfcstyle.github.io/2021/02/28/iOS-swift-type-erased/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)