一起养成写作习气!这是我参与「日新计划 4 月更文应战」的第8天,点击检查活动概况。

前语

Swift的总体目标是既强大到能够用于底层体系编程,又足够简略让初学者学习,这有时会导致恰当风趣的状况——当Swift的类型体系的力气要求咱们布置恰当高档的技能来处理乍一看或许更微不足道的问题。

大多数Swift开发人员会在某一时刻或另一时刻(通常是马上,而不是日后)会遇到这样一种状况,即需求某种形式的类型擦除才干引证通用协议。从本周开端,让咱们看一下是什么使类型擦除在Swift中成为必不可少的技能,然后持续探索完结它的不同 “风味(Flavors)”,以及每种风味为何各有优缺陷。

什么时候需求类型擦除?

一开端,“类型擦除”一词似乎与 Swift 给咱们的关注类型和编译时类型安全性的榜首感觉相反,因而,最好将其描述为躲藏类型,而不是彻底擦除它们。意图是使咱们能够更轻松地与通用协议进行交互,由于这些通用协议对将要完结它们的各种类型具有特定的要求。

以规范库中的Equatable协议为例。由于一切意图都是为了根据持平性比较两个相同类型的值,因而Self元类型为其唯一要求的参数:

protocol Equatable {
    static func ==(lhs: Self, rhs: Self) -> Bool
}

上面的代码使任何类型都能够契合Equatable,同时仍然需求==运算符两边的值都为同一类型,由于在完结上述办法时契合协议的每种类型都有必要“填写”自己的类型:

extension User: Equatable {
    static func ==(lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id
    }
}

该办法的优点在于,它不或许意外地比较两个不相关的持平类型(例如 UserString ),可是,它也导致不或许将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 {
    // 报错: 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) {
        ...
    }
}

上面的办法起作用了,由于现在编译器能够保证所传递的处理程序的确与作为恳求传递的Request完结兼容——由于它们都根据泛型R,而后者又被限制为契合Request协议。

可是,虽然咱们处理了办法的签名问题,但仍然无法对传递的恳求进行实践的处理,由于咱们无法将其存储为Request属性或[Request]数组,这将使持续构建咱们的RequestQueue变得困难。也就是说,除非咱们开端进行类型擦除。

通用包装器类型擦除

咱们将讨论的榜首种类型擦除实践上并没有涉及擦除任何类型,而是将它们包装在一个咱们能够更简略引证的通用类型中。持续从之前的RequestQueue示例开端,咱们首要创立该包装器类型——该包装器类型将捕获每个恳求的perform办法作为闭包,以及在恳求完结后应调用的处理程序:

// 这将使咱们将 Request 协议的完结包装在一个
// 与 Request 协议具有相同的响应和过错类型的泛型中
struct AnyRequest<Response, Error: Swift.Error> {
    typealias Handler = (Result<Response, Error>) -> Void
    let perform: (@escaping Handler) -> Void
    let handler: Handler
}

接下来,咱们还将把RequestQueue自身转换为相同的ResponseError类型的泛型——使得编译器能够保证一切关联的类型和泛型类型对齐,从而使咱们能够将恳求存储为独立的引证并作为数组的一部分——像这样:

class RequestQueue<Response, Error: Swift.Error> {
    private typealias TypeErasedRequest = AnyRequest<Response, Error>
    private var queue = [TypeErasedRequest]()
    private var ongoing: TypeErasedRequest?
    // 咱们修改了'add'办法,以包括一个'where'子句,
    // 该子句保证传递的恳求已关联的类型与行列的通用类型匹配。
    func add<R: Request>(
        _ request: R,
        handler: @escaping R.Handler
    ) where R.Response == Response, R.Error == Error {
        //要履行类型擦除,咱们只需创立一个实例'AnyRequest',
        //然后将其传递给根底恳求将“perform”办法与处理程序一起作为闭包。
        let typeErased = AnyRequest(
            perform: request.perform,
            handler: handler
        )
        // 由于咱们要完结行列,因而咱们不想一次有两个恳求,
        // 所以将恳求保存下拉,以防稍后有一个正在履行的恳求。
        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
            // 假如行列不为空,则履行下一个恳求
            ...
        }
    }
}

请注意,上面的示例以及本文中的其他示例代码都不是线程安全的——为了使工作变得简略。有关线程安全的更多信息,请检查“防止在Swift 中竞赛条件”。

上面的办法作用很好,但有一些缺陷。咱们不仅引进了新的AnyRequest类型,还需求将RequestQueue转换为泛型。这给咱们带来了一点灵活性,由于咱们现在只能将任何给定的行列用于具有相同 响应/过错类型 组合的恳求。具有挖苦意味的是,假如咱们想组成多个实例,将来或许还需求咱们自己完结行列擦除。

闭包类型擦除

咱们不引进包装类型,而是让咱们看一下如何运用闭包来完结相同的类型擦除,同时还要使咱们的RequestQueue非泛型且通用,足以用于不同类型的恳求。

运用闭包擦除类型时,其思想是捕获在闭包内部履行操作所需的一切类型信息,并使该闭包仅承受非泛型(乃至是Void)输入。这样一来,咱们就能够引证,存储和传递该功用,而无需实践知道功用内部会产生什么,从而为咱们供给了更强大的灵活性。

更新RequestQueue以运用根据闭包的类型擦除的办法如下:

class RequestQueue {
    private var queue = [() -> Void]()
    private var isPerformingRequest = false
    func add<R: Request>(_ request: R,
                         handler: @escaping R.Handler) {
        // 此闭包将同时捕获恳求及其处理程序,而不会露出任何类型信息
        // 在其外部,供给彻底的类型擦除。
        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()
    }
}

虽然过分依赖闭包来捕获功用和状态有时会使咱们的代码难以调试,但也或许使彻底封装类型信息成为或许——使得像RequestQueue这样的对象能够在没有真正了解在底层工作的类型的任何细节的状况下进行工作。

有关根据闭包的类型擦除及其更多不同办法的更多信息,请检查“Swift 运用闭包完结类型擦除”。

外部特化(External specialization)

到目前为止,咱们已经在RequestQueue自身中履行了一切类型擦除,这有一些优点——它能够让任何外部代码运用咱们的行列,而不需求知道咱们运用什么类型的类型擦除。但是,有时在将协议完结传递给API之前进行一些轻量级转换,既能够使工作变得更简略,又能够奇妙地封装类型擦除代码自身。

关于咱们的RequestQueue,一种办法是要求在将每个Request完结增加到行列之前对其进行特化——这将把它转换为RequestOperation,如下所示:

struct RequestOperation {
    fileprivate let closure: (@escaping () -> Void) -> Void
    func perform(then handler: @escaping () -> Void) {
        closure(handler)
    }
}

与咱们之前运用闭包在RequestQueue中履行类型擦除的方法相似,上面的RequestOperation类型将使咱们能够在扩展Request时履行该操作:

extension Request {
    func makeOperation(with handler: @escaping Handler) -> RequestOperation {
        return RequestOperation { finisher in
            // 咱们其实想在这里捕获'self',由于不这样话
            // 咱们将冒着无法保存基本恳求的危险。
            self.perform { result in
                handler(result)
                finisher()
            }
        }
    }
}

上述办法的优点在于,无论是公共API仍是内部完结,它都让咱们的RequestQueue愈加简略。它现在能够彻底专注于作为一个行列,而不必关怀任何类型的类型擦除:

class RequestQueue {
    private var queue = [RequestOperation]()
    private var ongoing: RequestOperation?
    // 由于类型擦除现在产生在request被传递给 queue 之前,
    // 它能够简略地承受一个详细的“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
            // 假如行列不为空,则履行下一个恳求
            ...
        }
    }
}

但是,这里的缺陷是,在将每个恳求增加到行列之前,咱们有必要手动将其转换为RequestOperation——虽然这不会在每个调用点增加很多代码,但这取决于有必要完结相同转换的次数,它最终或许会有点像样板。

结语

虽然 Swift 供给了一个功用强大得难以置信的类型体系,能够协助咱们防止很多的bug,但有时它会让人觉得咱们有必要与体系抗争,才干运用通用协议之类的功用。有必要进行类型擦除开始看起来像是一件不必要的庶务,但它也带来了一些好处——比如从不需求关怀这些类型的代码中躲藏特定类型信息。

在未来,咱们或许还会看到 Swift 中增加了新的特性,能够自动化创立类型擦除包装类型的进程,也能够通过使协议也被用作恰当的泛型(例如能够界说像Request<Response,Error>这样的协议)来消除对它的很多需求,而不仅仅依赖于相关的类型)。

什么样的类型擦除是最合适的——无论是现在仍是将来——当然很大程度上取决于上下文,以及咱们的功用是否能够在闭包中轻松地履行,或者完整包装器类型或泛型是否更适合这个问题。

感谢阅读!

译自 John Sundell 的 Different flavors of type erasure in Swift