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

 更新时间:2020年6月30日 23:37  点击:2511

前言

在 Swift 的世界中,如果我们将协议称之为国王,那么泛型则可以视作皇后,所谓一山不容二虎,当我们把这两者结合起来使用的时候,似乎会遇到极大的困难。那么是否有一种方法,能够将这两个概念结合在一起,以便让它们成为我们前进道路上的垫脚石,而不是碍手碍脚的呢?答案是有的,这里我们将会使用到类型擦除 (Type Erasure) 这个强大的特性。

你也许曾听过类型擦除,甚至也使用过标准库提供的类型擦除类型如 AnySequence。但到底什么是类型擦除? 如何自定义类型擦除? 在这篇文章中,我将讨论如何使用类型擦除以及如何自定义。在此感谢 Lorenzo Boaro 提出这个主题。

有时你想对外部调用者隐藏某个类的具体类型,或是一些实现细节。在一些情况下,这样做能防止静态类型在项目中滥用,或者保证了类型间的交互。类型擦除就是移除某个类的具体类型使其变得更通用的过程。

协议或抽象父类可作为类型擦除简单的实现方式之一。例如 NSString 就是一个例子,每次创建一个 NSString 实例时,这个对象并不是一个普通的 NSString 对象,它通常是某个具体的子类的实例,这个子类一般是私有的,同时这些细节通常是被隐藏起来的。你可以使用子类提供的功能而不用知道它具体的类型,你也没必要将你的代码与它们的具体类型联系起来。

在处理 Swift 泛型以及关联类型协议的时候,可能需要使用一些高级的内容。Swift 不允许把协议当做具体的类型来使用。例如,如果你想编写一个方法,它的参数是一个包含了 Int 的序列,那么下面这种做法是不正确的:

func f(seq: Sequence<Int>) { ...

你不能这样使用协议类型,这样会在编译时报错。但你可以使用泛型来替代协议,解决这个问题:

func f<S: Sequence>(seq: S) where S.Element == Int { ...

有时候这样写完全可以,但有些地方还存在一些比较麻烦的情况,通常你不可能只在一个地方添加泛型: 一个泛型函数对其他泛型要求更多… 更糟糕的是,你不能将泛型作为返回值或者属性。这就跟我们想的有点不一样了。

func g<S: Sequence>() -> S where S.Element == Int { ...

我们希望函数 g 能返回任何符合的类型,但上面这个不同,它允许调用者选择他所需要的类型,然后函数 g 来提供一个合适的值。

Swift 标准库中提供了 AnySequence 来帮助我们解决这个问题。AnySequence 包装了一个任意类型的序列,并擦除了它的类型。使用 AnySequence 来访问这个序列,我们来重写一下函数 f 与 函数 g:

func f(seq: AnySequence<Int>) { ...

func g() -> AnySequence<Int> { ...

泛型部分不见了,同时具体的类型也被隐藏起来了。由于使用了 AnySequence 包装具体的值,它带来了一定的代码复杂性以及运行时间成本。但是代码却更简洁了。

Swift 标准库中提供了很多这样的类型,如 AnyCollection、AnyHashable 及 AnyIndex。这些类型在你自定义泛型或协议的时候非常的管用,你也可以直接使用这些类型来简化你的代码。接下来让我们探索实现类型擦除的多种方式吧。

基于类的类型擦除

有时我们需要在不暴露类型信息的情况下从多个类型中包装一些公共的功能,这听起来就像是父类-子类的关系。事实上我们的确可以使用抽象父类来实现类型擦除。父类提供 API 接口,不用去管谁来实现。而子类根据具体的类型信息实现相应的功能。

接下来我们将使用这种方式来自定义 AnySequence,我们将其命名为 MAnySequence:

class MAnySequence<Element>: Sequence {

这个类需要一个 iterator 类型作为 makeIterator 返回类型。我们必须要做两次类型擦除来隐藏底层的序列类型以及迭代器的类型。我们在 MAnySequence 内部定义了一个 Iterator 类,该类遵循着 IteratorProtocol 协议,并在 next() 方法中使用 fatalError 抛出异常。Swift 本身不支持抽象类型,但这样也够了:

class Iterator: IteratorProtocol {
 func next() -> Element? {
 fatalError("Must override next()")
 }
}

MAnySequence 对 makeIterator 方法实现也差不多。直接调用将抛出异常,这用来提示子类需要重写这个方法:

 func makeIterator() -> Iterator {
 fatalError("Must override makeIterator()")
 }
}

这样就定义了一个基于类的类型擦除的API,私有的子类将来实现这些API。公共类通过元素类型参数化,但私有实现类由它包装的序列类型进行参数化:

private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<Seq.Element> {

MAnySequenceImpl 需要一个继承于 Iterator 的子类:

class IteratorImpl: Iterator {

IteratorImpl 包装了序列的迭代器:

var wrapped: Seq.Iterator

init(_ wrapped: Seq.Iterator) {
 self.wrapped = wrapped
}

在 next 方法中调用被包装的序列迭代器:

 override func next() -> Seq.Element? {
 return wrapped.next()
 }
}

相似地,MAnySequenceImpl 包装一个序列:

var seq: Seq

init(_ seq: Seq) {
 self.seq = seq
}

从序列中获取迭代器,然后将迭代器包装成 IteratorImpl 对象返回,这样就实现了 makeIterator 的功能。

 override func makeIterator() -> IteratorImpl {
 return IteratorImpl(seq.makeIterator())
 }

}

我们需要一种方法来实际创建这些东西:对 MAnySequence 添加一个静态方法,该方法创建一个 MAnySequenceImpl 实例,并将其作为 MAnySequence 类型返回给调用者。

extension MAnySequence {
 static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where Seq.Element == Element {
 return MAnySequenceImpl<Seq>(seq)
 }
}

在实际开发中,我们可能会做一些额外的操作来让 MAnySequence 提供一个初始化方法。

我们来试试 MAnySequence:

func printInts(_ seq: MAnySequence<Int>) {
 for elt in seq {
 print(elt)
 }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence.make(array))
printInts(MAnySequence.make(array[1 ..< 4]))

完美!

基于函数的类型擦除

有时我们希望对外暴露支持多种类型的方法,但又不想指定具体的类型。一个简单的办法就是,存储那些签名仅涉及到我们想公开的类型的函数,函数主体在底层已知具体实现类型的上下文中创建。

我们一起看看如何运用这种方法来设计 MAnySequence,与前面的实现很类似。它是一个结构体而非类,这是因为它仅仅作为容器使用,不需要有任何的继承关系。

struct MAnySequence<Element>: Sequence {

跟之前一样,MAnySequence 也需要一个可返回的迭代器(Iterator)。迭代器同样被设计为结构体,并持有一个参数为空并返回 Element? 的存储型属性,实际上这个属性是一个函数,被用于 IteratorProtocol 协议的 next 方法中。接下来 Iterator 遵循 IteratorProtocol 协议,并在 next 方法中调用函数:

struct Iterator: IteratorProtocol {
 let _next: () -> Element?

 func next() -> Element? {
  return _next()
 }
}

MAnySequence 与 Iterator 很相似:持有一个参数为空返回 Iterator 类型的存储型属性。遵循 Sequence 协议并在 makeIterator 方法中调用这个属性。

let _makeIterator: () -> Iterator

func makeIterator() -> Iterator {
 return _makeIterator()
}

MAnySequence 的构造函数正是魔法起作用的地方,它接收任意序列作为参数:

init<Seq: Sequence>(_ seq: Seq) where Seq.Element == Element {

接下来需要在构造函数中包装此序列的功能:

_makeIterator = {

如何生成迭代器?请求 Seq 序列生成:

var iterator = seq.makeIterator()

接下来我们利用自定义的迭代结构体包装序列生成的迭代器,包装后的 _next 属性将会在迭代器协议的 next() 方法中被调用:

   return Iterator(_next: { iterator.next() })
  }
 }
}

接下来展示如何使用 MAnySequence:

func printInts(_ seq: MAnySequence<Int>) {
 for elt in seq {
  print(elt)
 }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence(array))
printInts(MAnySequence(array[1 ..< 4]))

正确运行,太棒了!

当需要将小部分功能包装为更大类型的一部分时,这种基于函数的类型擦除方法特别实用,这样做就不需要有单独的类来实现被擦除类型的这部分功能了。

比方说你现在想要编写一些适用于各种集合类型的代码,但它真正需要能够对这些集合执行的操作是获取计数并执行从零开始的整数下标。如访问 tableView 数据源。它可能看起来像这样:

class GenericDataSource<Element> {
 let count: () -> Int
 let getElement: (Int) -> Element

 init<C: Collection>(_ c: C) where C.Element == Element,C.Index == Int {
  count = { c.count }
  getElement = { c[$0 - c.startIndex] }
 }
}

GenericDataSource 其他代码可通过调用 count() 或 getElement() 来操作传入的集合。且不会让集合类型破坏 GenericDataSource 泛型参数。

结束语

类型擦除是一种非常有用的技术,它可用来阻止泛型对代码的侵入,也可用来保证接口简单明了。通过将底层类型包装起来,将API与具体的功能进行拆分。这可以通过使用抽象的公共超类和私有子类或将 API 包装在函数中来实现。对于只需要一些功能的简单情况,基于函数类型擦除极其有效。

Swift 标准库提供了几种可直接利用的类型擦除类型。如 AnySequence 包装一个 Sequence,正如其名,AnySequence 允许你对序列迭代而无需知道序列具体的类型。AnyIterator 也是类型擦除的类型,它提供一个类型擦除的迭代器。AnyHashable 也同样是类型擦除的类型,它提供了对Hashable类型访问功能。Swift 还有很多基于集合的擦除类型,你可以通过搜索 Any 来查阅。标准库中也为 Codable API 设计了类型擦除类型: KeyedEncodingContainer 和 KeyedDecodingContainer。它们都是容器协议类型包装器,可用来在不知道底层具体类型信息的情况下实现 Encode 和 Decode。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对猪先飞的支持。

作者:Mike Ash,原文链接,原文日期:2017-12-18

译者:rsenjoyer;校对:Yousanflics,numbbbbb;定稿:Forelax

[!--infotagslink--]

相关文章

  • swift中利用runtime交换方法的实现示例

    这篇文章主要给大家介绍了关于swift中利用runtime交换方法的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。...2020-06-30
  • Swift设置UILabel内边距的实例代码

    有时候,我们需要一个显示文字,又想这些文字与边界之间有自定义的边距,所以下面这篇文章主要给大家介绍了关于Swift设置UILabel内边距的相关资料,需要的朋友可以参考下...2021-10-14
  • swift中的@UIApplicationMain示例详解

    这篇文章主要给大家介绍了关于swift中@UIApplicationMain的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。...2020-06-30
  • 详解swift中xcworkspace多项目管理

    给大家详细讲解了IOS开发中swift语言xcworkspace多项目管理的方法和介绍,一起参考一下。...2020-06-30
  • Swift 中如何使用 Option Pattern 改善可选项的 API 设计

    这篇文章主要介绍了Swift 中如何使用 Option Pattern 改善可选项的 API 设计,帮助大家更好的进行ios开发,感兴趣的朋友可以了解下...2020-10-23
  • 如何使用Swift来实现一个命令行工具的方法

    这篇文章主要介绍了如何使用Swift来实现一个命令行工具,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-30
  • Swift实现多个TableView侧滑与切换效果

    这篇文章主要为大家详细介绍了Swift实现多个TableView侧滑与切换效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-06-30
  • Swift中的命名空间详解

    这篇文章主要给大家介绍了关于Swift中命名空间的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-30
  • 浅析Swift中struct与class的区别(汇编角度底层分析)

    这篇文章主要介绍了Swift中struct与class的区别 ,本文从汇编角度分析struct与class的区别,通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-30
  • Swift缩放并填充图片功能的实现

    最近有一个需求,就是将图片先等比例缩放到指定大小,然后将空余出来空间填充为黑色,返回指定大小的图片。本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧...2021-11-02
  • Swift实现倒计时5秒功能

    这篇文章主要为大家详细介绍了Swift实现倒计时5秒功能,在“登录”和“注册”页面也有相似功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-06-30
  • Swift 使用 Observe 监测页面滚动的实现方法

    这篇文章主要介绍了Swift 使用 Observe 监测页面滚动的实现方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-30
  • Swift在控件中添加点击手势的方法

    这篇文章主要介绍了Swift在控件中添加点击手势的方法,本文讲解如何在tableview的headerview中添加点击手势的方法,需要的朋友可以参考下...2020-06-30
  • Swift中优雅处理闭包导致的循环引用详解

    这篇文章主要给大家介绍了关于Swift中优雅的处理闭包导致的循环引用的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Swift具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧...2020-06-30
  • Swift如何使用类型擦除及自定义详解

    有很多地方会用到类型擦除,并且它们的作用的各不相同。下面这篇文章主要给大家介绍了关于Swift如何使用类型擦除及自定义的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下...2020-06-30
  • Swift利用CoreData如何存储多种数据类的通讯录

    这篇文章主要给大家介绍了关于Swift利用CoreData如何存储多种数据类的通讯录的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧。...2020-06-30
  • SwiftUI 中创建反弹动画的实现

    这篇文章主要介绍了SwiftUI 中创建反弹动画的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-30
  • Swift中动态调用实例方法介绍

    这篇文章主要介绍了Swift中动态调用实例方法介绍,在Swift中有一类很有意思的写法,可以让我们不直接使用实例来调用这个实例上的方法,而是通过类型取出这个类型的某个实例方法的签名,然后再通过传递实例来拿到实际需要调用的方法,需要的朋友可以参考下...2020-06-30
  • iOS Swift UICollectionView横向分页滚动,cell左右排版问题详解

    UICollectionView是iOS中比较常见的一个控件,这篇文章主要给大家介绍了关于iOS Swift UICollectionView横向分页滚动,cell左右排版问题的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随小编来一起学习学习吧。...2020-06-30
  • Swift中如何避免循环引用的方法

    本篇文章主要介绍了Swift中如何避免循环引用的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-30