前言Swift的总体目标是对于低级系统编程足够强大,同时对于初学者来说足够容易学习,这有时会导致相当有趣的情况——当Swift的类型系统的强大功能法律要求我们部署相当先进的技术来解决乍一看似乎更微不足道的问题。大多数Swift开发人员都会在某个时候(通常是马上,而不是稍后)遇到需要某种形式的类型擦除以引用通用协议的情况。让我们从本周开始,看看是什么让类型擦除成为Swift中的一项基本技术,然后继续探索实现它的不同“风格”,以及为什么每种风格都有自己的优缺点。什么时候需要类型擦除?乍一看,“类型擦除”一词似乎与Swift给我们的第一印象有悖常理,即关注类型和编译时类型安全,因此将其描述为隐藏类型比完全擦除它们更好。目的是让我们更容易地与通用协议交互,这些通用协议对实现它们的各种类型有特定的要求。以标准库中的Equatable协议为例。由于所有目的都是基于相等性来比较相同类型的两个值,因此Self元类型是唯一需要的参数:protocolEquatable{staticfunc==(lhs:Self,rhs:Self)->Bool}上面的代码使得任何类型都可以符合Equatable,同时仍然要求==运算符两边的值是相同的类型,因为每个符合协议的类型在时都必须“填写”自己的类型实现上面的方法:extensionUser:Equatable{staticfunc==(lhs:User,rhs:User)->Bool{returnlhs.id==rhs.id}}这个方法的好处是不会不小心比较两个不相关的相等类型(例如User和String),但是,它也使得无法将Equatable引用为独立协议(例如create[Equatable]),因为编译器需要知道实际符合协议的确切类型为了使用它。当协议包含关联类型时也是如此。例如,这里我们定义了一个Request协议,它允许我们将各种形式的数据请求(例如网络调用、数据库查询和缓存获取)隐藏在一个统一的实现中:=(Result)->Voidfuncperform(thenhandler:@escapingHandler)}上面的方法为我们提供了与Equatable相同的权衡——它非常强大,因为它允许我们为任何类型的请求创建一个通用抽象,但也使得无法直接引用Request协议本身,例如:classRequestQueue{//Error:protocol'Request'canonlybeusedasageneric//constraintbecauseithasSelforassociatedtyperequirementsfuncadd(_request:Request,handler:@escapingRequest.Handler){...}}解决上述问题的一种方法是完全按照报错信息的内容进行操作,即不直接引用Request,使用我t作为一般约束:classRequestQueue{funcadd(_request:R,handler:@escapingR.Handler){...}}上面的工作是因为现在编译器可以保证传递的处理程序是确实与作为请求传递的请求实现兼容——因为它们都是基于通用R的,而R又被限制为符合请求协议。然而,即使我们修复了方法的签名,我们仍然无法对传递的请求进行实际处理,因为我们无法将其存储为Request属性或[Request]数组,这将使我们难以继续构建RequestQueue。也就是说,除非我们开始类型擦除。通用包装器类型擦除我们将探索的第一种类型擦除实际上并不涉及擦除任何类型,而是将它们包装在我们可以更轻松地引用的通用类型中。继续前面的RequestQueue示例,我们首先创建这个包装器类型-这个包装器类型将捕获每个请求的执行方法作为一个闭包,以及一个应该在请求完成时调用的处理程序://这将允许我们包装通用请求协议//具有与请求协议相同的响应和错误类型Handler)->voidlethandler:Handler}接下来,我们还将RequestQueue本身转换为相同Response和Error类型的泛型——这样编译器可以保证所有关联类型和泛型的类型对齐,这样我们就可以将请求存储为单独的引用并作为数组的一部分——像这样:eErasedRequest?//我们修改了'add'方法以包含一个'where'子句,该子句//确保传递的请求的关联类型与队列的通用类型相匹配。funcadd(_request:R,handler:@escapingR.Handler)whereR.Response==Response,R.Error==Error{//要执行类型擦除,我们只需创建一个实例'AnyRequest',//然后将其作为闭包与处理程序一起传递给底层请求的“执行”方法。lettypeErased=AnyRequest(perform:request.perform,handler:handler)//因为我们正在实现一个队列,我们??不希望一次有两个请求,//所以保存下拉列表以防有一个正在执行的请求之后。guardongoing==nilelse{queue.append(typeErased)return}perform(typeErased)}privatefuncperform(_request:TypeErasedRequest){ongoing=requestrequest.perform{[weakself]结果为request.handler(result)self?.ongoing=nil//如果队列不为空,则执行下一个请求...}}}请注意,上面的示例和本文中的示例代码的其余部分都不是线程安全的-为简单起见。有关线程安全的更多信息,请查看“AvoidingRaceConditionsinSwift”。上述方法效果很好,但也有一些缺点。我们不仅引入了新的AnyRequest类型,还需要将RequestQueue转换为泛型。这给了我们一点灵活性,因为我们现在只能将任何给定队列用于具有相同响应/错误类型组合的请求。具有讽刺意味的是,如果我们要组合多个实例,我们将来可能还需要自己实现队列擦除。闭包类型擦除不引入包装器类型,让我们看看如何使用闭包来实现相同类型的擦除,同时还使我们的RequestQueue非泛型且足够泛型以用于不同类型的请求。当使用闭包擦除类型时,想法是捕获在闭包内执行操作所需的所有类型信息,并让该闭包仅接受非泛型(甚至Void)输入。这允许我们引用、存储和传递函数而无需实际知道函数内部发生了什么,从而为我们提供了更多的灵活性。以下是如何更新RequestQueue以使用基于闭包的类型擦除:escapingR.Handler){//此闭包将捕获请求及其处理程序,而不会在其外部暴露任何类型信息,从而提供完整的类型擦除。lettypeErased={request.perform{[weakself]resultinhandler(result)self?.isPerformingRequest=falseself?.performNextIfNeeded()}}queue.append(typeErased)performNextIfNeeded()}privatefuncperformNextIfNeeded(){守卫!isPerformingRequest&&!queue.isEmptyelse{return}isPerformingRequest=trueletclosure=queue.removeFirst()closure()}}虽然过度依赖闭包来捕获功能和状态有时会使我们的代码难以调试,但它也可以使类型信息的完全封装成为可能——允许像RequestQueue这样的对象工作,而无需真正了解在引擎盖下工作的类型的任何细节。有关基于闭包的类型擦除及其许多不同方法的更多信息,请查看“使用Swift中的闭包进行类型擦除”。外部特化到目前为止,我们已经在RequestQueue本身中执行了所有类型擦除,这有一些优点-它允许任何外部代码使用我们的队列而无需知道是什么类型的类型擦除。然而,有时在将协议实现传递给API之前进行一些轻量级转换既可以使事情变得更简单,又可以巧妙地封装类型擦除代码本身。对于我们的RequestQueue,一种方法是要求每个Request实现在将其添加到队列之前进行专门化——这会将其变成RequestOperation,如下所示:structRequestOperation{fileprivateletclosure:(@escaping()->Void)->Voidfuncperform(thenhandler:@escaping()->Void){closure(handler)}}类似于我们之前在RequestQueue中使用闭包进行类型擦除,上面的RequestOperation类型可以让我们进行操作当扩展Request时:extensionRequest{funcmakeOperation(withhandler:@escapingHandler)->RequestOperation{returnRequestOperation{finisherin//我们实际上想在这里捕获'self'因为它没有//我们将冒无法坚持基本请求。self.perform{resultinhandler(result)finisher()}}}}上述方法的优点在于它使我们的RequestQueue在公共API和内部实现中都更加简单。它现在可以完全专注于成为一个队列,而不必关心任何类型的擦除://因为现在在传递请求时会发生类型擦除在给队列之前,//它可以简单地接受一个具体的“RequestOperation”实例。funcadd(_operation:RequestOperation){guardongoing==nilelse{队列。append(operation)return}perform(operation)}privatefuncperform(_operation:RequestOperation){ongoing=operation操作。perform{[weakself]inself?.ongoing=nil//如果队列不为空,执行下一个请求...}}}但是,这里的缺点是我们必须手动将每个请求转换为RequestOperation-而这不会在每个调用站点添加大量代码,具体取决于必须完成相同转换的次数,它最终可能会有点样板。结论尽管Swift提供了一个非常强大的类型系统来帮助我们避免大量的错误,但有时我们会觉得我们必须与系统作斗争才能使用通用协议等功能。必须进行类型擦除最初看起来像是一件不必要的苦差事,但它也带来了好处——比如从不需要关心这些类型的代码中隐藏特定于类型的信息。将来,我们可能还会看到Swift添加了新功能,可以自动创建类型擦除包装类型的过程,也可以通过使协议用作适当的泛型(例如能够定义像Request这样的协议)来删除批量需要它而不是仅仅依赖于关联的类型)。什么样的类型擦除是最合适的——无论是现在还是将来——当然在很大程度上取决于上下文,以及我们的功能是否可以很容易地在闭包中执行,或者完整的包装类型或泛型是否更适合这个问题。