之前在一些分享会上经常听到 类型擦除 (Type Erase)这个概念,从其命名上大概知道它要干什么,但是对于为什么要用它?以及什么场景下使用它?对此,我并没有深刻的理解。于是,借着假期好好研究了一下。问题的一切要从泛型协议说起。
协议如何支持泛型?
我们知道,在 Swift 中,protocol 支持泛型的方式与 class/struct/enum 不同,具体说来:
分别如下所示:
1 |
// class |
这时候我们可能会有一个疑问:为什么 class/enum/struct 使用泛型参数,而 protocol 则使用抽象类型成员?我查阅了很多讨论,原因可以归纳为两点:
Array<Int>
,
Array<String>
,很显然类型参数适用于多次表达。然而,协议的表达是一次性的,我们只会实现
GenericProtocol
,而不会特定地实现
GenericProtocol<Int>
或
GenericProtocol<String>
。
GenericProtocol
约束了
next()
方法的返回类型,而不是定义
GenericProtocol
的类型。而抽象类型成员则可以用来实现类型约束的。
如何存储非泛型协议?
下面,我们来看一下协议的存储。首先,我们来考虑非泛型协议。
1 |
protocol Drawable { |
Existential Container
。
Existential Container
对具体类型进行封装,从而实现存储一致性。关于 Existential Container
的具体内容,可以参考
《Swift性能优化(2)——协议与泛型的实现》
。
如何存储泛型协议?
接下来,我们再来考虑泛型协议的存储。
1 |
protocol Generator { |
Existential Container
类型可以保证存储一致性。
事实上,上述代码从表面上看的确不会有问题,但是我们忽略了泛型协议的本质——约束类型。我们可以在上述代码的基础上,继续加上如下代码:
1 |
let x = value.generate() |
Generator
协议约束了
generate()
方法的返回类型,在本例中,
x
的类型既可能是
Int
,又可能是
String
。而 Swift
本身又是一种强类型语言,所有的类型必须在编译时确定。因此,swift
无法直接支持泛型协议的存储。
所以,在实际开发中,Xcode 会对以下这种类型的定义报错。
1 |
let value: Generator = IntGenerator() |
那么,如何解决泛型协议的存储呢?
问题的本质是要将泛型协议的所约束的类型进行擦除,即 类型擦除 (Type Erase) ,从而骗过编译器,解决该问题的思路有两种:
对于『泛型协议转换成非泛型协议』,由于泛型协议的实现采用的是抽象类型成员,而不是类型参数,只能基于抽象类型成员进行泛型约束,然而通过转换而来的协议本质上仍然是泛型协议,如下所示。此方法无效。
1 |
protocol BoolGenerator: Generator where AbstractType == String { |
对于『泛型协议封装成的具体类型』,事实上,这是业界普遍的解决方案,swift 中很多系统库都是采用这种思路来解决的。
为此,我们可以使用 thunk 技术来解决。什么是 thunk? 一个 thunk 通常是一个子程序,它被创造出来,用于协助调用其他的子程序 。说到底,就是通过创造一个中间层来解决遇到的问题。
thunk 技术应用非常广泛,比如:oc swift 混编时,我们可以在调用栈中看到存在 thunk 函数。
具体的解决方法是:
1 |
protocol Generator { |
当我们拥有一个 thunk,我们可以把它当做类型使用(需要提供具体类型)。
1 |
struct StringGenerator: Generator { |
采用 thunk 技术,我们把泛型协议封装成的具体类型,其本质就是对泛型协议进行了 类型擦除(Type Erase) ,从而解决了泛型类型的存储问题。
关于类型擦除,在 Swift 标准库的实现中,一般会创建一个包装类型(class
struct)将遵循了协议的对象进行封装。包装类型本身也遵循协议,它会将对协议方法的调用传递到内部的对象中。包装类型一般命名为
Any{protocol-name}
,如:
AnySequence
、
AnyCollection
。
下面,是以 Swift 标准库的方式对泛型协议进行类型擦除。
1 |
protocol Printer { |
AnyPrinter
并没有显式地引用
base
实例。事实上我们也不能这么做,因为我们不能在
AnyPrinter
中声明一个
Printer<T>
的属性。对此,我们使用一个方法指针
_print
指向了
base
的
print
方法,通过这种方式,
base
被柯里化成了
self
,从而隐式地引用了
base
实例。
在 RxSwift 中,就有针对泛型协议类型擦除的相关应用,我们来看下面这段代码:
1 |
public protocol ObserverType { |
ObserverType
是一个泛型协议,
AnyObserver
是一个用于类型擦除的包装类型。
AnyObserver
定义了方法指针(闭包),向实现协议的抽象类型实例所声明的方法。同时
AnyObserver
自身又遵循
ObserverType
协议,在调用
AnyObserver
对应的协议时,它会将方法调用转发至对应方法指针所对应的方法。
除了
AnyObserver
之外,
Observable
同样也是一个用于类型擦除的包装类型,其工作原理也是基本相似。
此外,swift
标准库中也大量应用了类型擦除,比如:
AnySequence
、
AnyIterator
、
AnyIndex
、
AnyHashable
、
AnyCollection
等等。后续有时间,我们再来看看标准库中对于泛型协议的类型擦除是怎么做,可以肯定的是,其实现原理基本是一致的
本文,我们通过泛型协议的例子,了解了类型擦除的作用。这里,类型擦除将泛型协议所关联的类型信息进行了擦除,本质上是通过类型参数的方式,让实现抽象类型成员具体化。在面向协议编程中,类型擦除也是一种非常常见的手段,后续我们阅读相关代码时,也就不会对包装类型产生迷惑了。