Swift中风味各异的类型擦除实例详解

目录
  • 前言
  • 什么时候需要类型擦除?
  • 通用包装器类型擦除
  • 闭包类型擦除
  • 结语

前言

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
    }
}

该方法的优点在于,它不可能意外地比较两个不相关的相等类型(例如 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 {
    // 报错: 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本身转换为相同的Response和Error类型的泛型——使得编译器可以保证所有关联的类型和泛型类型对齐,从而使我们可以将请求存储为独立的引用并作为数组的一部分——像这样:

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这样的协议)来消除对它的大量需求,而不仅仅依赖于相关的类型)。

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

到此这篇关于Swift中风味各异的类型擦除的文章就介绍到这了,更多相关Swift类型擦除内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Swift如何使用类型擦除及自定义详解

    前言 在 Swift 的世界中,如果我们将协议称之为国王,那么泛型则可以视作皇后,所谓一山不容二虎,当我们把这两者结合起来使用的时候,似乎会遇到极大的困难.那么是否有一种方法,能够将这两个概念结合在一起,以便让它们成为我们前进道路上的垫脚石,而不是碍手碍脚的呢?答案是有的,这里我们将会使用到类型擦除 (Type Erasure) 这个强大的特性. 你也许曾听过类型擦除,甚至也使用过标准库提供的类型擦除类型如 AnySequence.但到底什么是类型擦除? 如何自定义类型擦除? 在这篇文章中,我将

  • Swift中风味各异的类型擦除实例详解

    目录 前言 什么时候需要类型擦除? 通用包装器类型擦除 闭包类型擦除 结语 前言 Swift的总体目标是既强大到可以用于底层系统编程,又足够容易让初学者学习,这有时会导致相当有趣的情况——当Swift的类型系统的力量要求我们部署相当高级的技术来解决乍一看可能更微不足道的问题. 大多数Swift开发人员会在某一时刻或另一时刻(通常是马上,而不是日后)会遇到这样一种情况,即需要某种形式的类型擦除才能引用通用协议.从本周开始,让我们看一下是什么使类型擦除在Swift中成为必不可少的技术,然后继续探索实

  • Java泛型之类型擦除实例详解

    目录 前言 泛型是什么? 泛型的定义和使用 泛型类 泛型方法 泛型类与泛型方法的共存现象 泛型接口 通配符 ? 无限定通配符 <?> <? extends T> 类型擦除 类型擦除带来的局限性 泛型中值得注意的地方 Java 不能创建具体类型的泛型数组 泛型,并不神奇 总结 前言 泛型,一个孤独的守门者. 大家可能会有疑问,我为什么叫做泛型是一个守门者.这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇.泛型是 Java 中一个很小巧的概念,但同时

  • C++ 中引用与指针的区别实例详解

    C++ 中引用与指针的区别实例详解 引用是从C++才引入的,在C中不存在.为了搞清楚引用的概念,得先搞明白变量的定义及引用与变量的区别,变量的要素一共有两个:名称与空间. 引用不是变量,它仅仅是变量的别名,没有自己独立的空间,它只符合变量的"名称"这个要素,而"空间"这个要素并不满足.换句话说,引用需要与它所引用的变量共享同一个内存空间,对引用所做的改变实际上是对所引用的变量做出修改.并且引用在定义的时候就必须被初始化.     参数传递的类型及相关要点: 1 按值

  • Angular中$cacheFactory的作用和用法实例详解

    先说下缓存: 一个缓存就是一个组件,它可以透明地储存数据,以便以后可以更快地服务于请求.多次重复地获取资源可能会导致数据重复,消耗时间.因此缓存适用于变化性不大的一些数据,缓存能够服务的请求越多,整体系统性能就能提升越多. $cacheFactory介绍: $cacheFactory是一个为Angular服务生产缓存对象的服务.要创建一个缓存对象,可以使用$cacheFactory通过一个ID和capacity.其中,ID是一个缓存对象的名称,capacity则是描述缓存键值对的最大数量. 1.

  • C++ 中引用和指针的关系实例详解

    C++ 中引用和指针的关系实例详解 1.引用在定义时必须初始化,指针没有要求 int &rNum; //未初始化不能通过编译 int *pNum; //可以 2. 一旦一个引用被初始化为指向一个对象,就不能再指向 其他对象,而指针可以在任何时候指向任何一个同类型对象 int iNum = 10; int iNum2 = 20; int &rNum = iNum; &rNum = iNum2; //不能通过 3. 没有NULL引用,但有NULL指针. int *pNum = NULL

  • Spring MVC自定义日期类型转换器实例详解

    Spring MVC自定义日期类型转换器实例详解 WEB层采用Spring MVC框架,将查询到的数据传递给APP端或客户端,这没啥,但是坑的是实体类中有日期类型的属性,但是你必须提前格式化好之后返回给它们.说真的,以前真没这样做过,之前都是一口气查询到数据,然后在jsp页面上格式化,最后展示给用户.但是这次不同,这次我纯属操作数据,没有页面.直接从数据库拿数据给它们返数据.它们给我传数据我持久化数据,说到这里一个小问题就默默的来了. 首先把问题还原一下吧(这是一个数据导出功能),下图中用红框圈

  • Java中JDBC实现动态查询的实例详解

    一 概述 1.什么是动态查询? 从多个查询条件中随机选择若干个组合成一个DQL语句进行查询,这一过程叫做动态查询. 2.动态查询的难点 可供选择的查询条件多,组合情况多,难以一一列举. 3.最终查询语句的构成 一旦用户向查询条件中输入数据,该查询条件就成为最终条件的一部分. 二 基本原理 1.SQL基本框架 无论查询条件如何,查询字段与数据库是固定不变的,这些固定不变的内容构成SQL语句的基本框架,如 select column... from table. 2.StringBuilder形成D

  • Java 中DateUtils日期工具类的实例详解

    Java 中DateUtils日期工具类的实例详解 介绍 在java中队日期类型的处理并不方便,通常都需要借助java.text.SimpleDateFormat类来实现日期类型 和字符串类型之间的转换,但是在jdk1.8之后有所改善,jdk1.7以及之前的版本处理日期类型并不方便, 可以借助Joda Time组件来处理,尤其是日期类型的一些数学操作就更是不方便. java代码 /** * * 日期工具类 java对日期的操作一直都很不理想,直到jdk1.8之后才有了本质的改变. * 如果使用的

  • Java 中HttpURLConnection附件上传的实例详解

    Java 中HttpURLConnection附件上传的实例详解 整合了一个自己写的采用Http做附件上传的工具,分享一下! 示例代码: /** * 以Http协议传输文件 * * @author mingxue.zhang@163.com * */ public class HttpPostUtil { private final static char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJK

  • java 中 String format 和Math类实例详解

    java 中 String format 和Math类实例详解 java字符串格式化输出 @Test public void test() { // TODO Auto-generated method stub //可用printf(); System.out.println(String.format("I am %s", "jj")); //%s字符串 System.out.println(String.format("首字母是 %c",

随机推荐