Swift 泛型协议
之前在一些分享会上经常听到 类型擦除(Type Erase)这个概念,从其命名上大概知道它要干什么,但是对于为什么要用它?以及什么场景下使用它?对此,我并没有深刻的理解。于是,借着假期好好研究了一下。问题的一切要从泛型协议说起。
协议如何支持泛型?
我们知道,在 Swift 中,protocol 支持泛型的方式与 class/struct/enum 不同,具体说来:
- 对于 class/struct/enum,其采用 类型参数(Type Parameters) 的方式。
- 对于 protocol,其采用 抽象类型成员(Abstract Type Member) 的方式,具体技术称为 关联类型(Associated Type)。
分别如下所示:
1 | // class |
这时候我们可能会有一个疑问:为什么 class/enum/struct 使用泛型参数,而 protocol 则使用抽象类型成员?我查阅了很多讨论,原因可以归纳为两点:
- 采用类型参数的泛型其实是定义了整个类型家族,我们可以通过传入类型参数可以转换成具体类型(类似于函数调用时传入不同参数),如:
Array<Int>
,Array<String>
,很显然类型参数适用于多次表达。然而,协议的表达是一次性的,我们只会实现GenericProtocol
,而不会特定地实现GenericProtocol<Int>
或GenericProtocol<String>
。 - 协议在 Swift 中有两个目的,第一个目的是
用来实现多继承(Swift
语言被设计成单继承),第二个目的是
强制实现者必须遵守协议所指定的泛型约束。很明显,协议并不是用来表示某种类型,而是用来约束某种类型,比如:
GenericProtocol
约束了next()
方法的返回类型,而不是定义GenericProtocol
的类型。而抽象类型成员则可以用来实现类型约束的。
如何存储非泛型协议?
下面,我们来看一下协议的存储。首先,我们来考虑非泛型协议。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15protocol Drawable {
func draw()
}
struct Point: Drawable {
var x, y: Double
func draw() { ... }
}
struct Line: Drawable {
var x1, y1, x2, y2: Double
func draw() { ... }
}
let value: Drawable = arc4random()%2 == 0 ? Point(x: 0, y: 0) : Line(x1: 0, y1: 0, x2: 1, y2: 1)Existential Container
。Existential Container
对具体类型进行封装,从而实现存储一致性。关于 Existential Container
的具体内容,可以参考《Swift性能优化(2)——协议与泛型的实现》。
如何存储泛型协议?
接下来,我们再来考虑泛型协议的存储。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22protocol Generator {
associatedtype AbstractType
func generate() -> AbstractType
}
struct IntGenerator: Generator {
typealias AbstractType = Int
func generate() -> Int {
return 0
}
}
struct StringGenerator: Generator {
typealias AbstractType = String
func generate() -> String {
return "zero"
}
}
let value: Generator = arc4random()%2 == 0 ? IntGenerator() : StringStore()Existential Container
类型可以保证存储一致性。
事实上,上述代码从表面上看的确不会有问题,但是我们忽略了泛型协议的本质——约束类型。我们可以在上述代码的基础上,继续加上如下代码:
1
let x = value.generate()
Generator
协议约束了
generate()
方法的返回类型,在本例中,x
的类型既可能是 Int
,又可能是 String
。而 Swift
本身又是一种强类型语言,所有的类型必须在编译时确定。因此,swift
无法直接支持泛型协议的存储。
所以,在实际开发中,Xcode 会对以下这种类型的定义报错。
1
2let value: Generator = IntGenerator()
// Error: Protocol 'Generator' can only be used as a generic constraint because it has Self or associated type requirements
那么,如何解决泛型协议的存储呢?
解决方法
问题的本质是要将泛型协议的所约束的类型进行擦除,即 类型擦除 (Type Erase),从而骗过编译器,解决该问题的思路有两种:
- 泛型协议转换成非泛型协议。
- 泛型协议封装成的具体类型。
对于『泛型协议转换成非泛型协议』,由于泛型协议的实现采用的是抽象类型成员,而不是类型参数,只能基于抽象类型成员进行泛型约束,然而通过转换而来的协议本质上仍然是泛型协议,如下所示。此方法无效。
1
2
3
4
5
6
7
8
9
10
11protocol BoolGenerator: Generator where AbstractType == String {
}
struct BoolGeneratorObj: BoolGenerator {
func generate() -> String {
return "bool"
}
}
let value: BoolGenerator = BoolGeneratorObj()
// Error: Protocol 'BoolGenerator' can only be used as a generic constraint because it has Self or associated type requirements
对于『泛型协议封装成的具体类型』,事实上,这是业界普遍的解决方案,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29protocol Printer {
associatedtype T
func print(val: T)
}
struct AnyPrinter<U>: Printer {
typealias T = U
private let _print: (U) -> ()
init<Base: Printer>(base : Base) where Base.T == U {
_print = base.print
}
func print(val: T) {
_print(val)
}
}
struct Logger<U>: Printer {
typealias T = U
func print(val: T) {
NSLog("\(val)")
}
}
let logger = Logger<Int>()
let printer = AnyPrinter(base: logger)
printer.print(5) // prints 5AnyPrinter
并没有显式地引用 base
实例。事实上我们也不能这么做,因为我们不能在 AnyPrinter
中声明一个 Printer<T>
的属性。对此,我们使用一个方法指针 _print
指向了
base
的 print
方法,通过这种方式,base
被柯里化成了
self
,从而隐式地引用了 base
实例。
具体应用
在 RxSwift
中,就有针对泛型协议类型擦除的相关应用,我们来看下面这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public protocol ObserverType {
/// The type of elements in sequence that observer can observe.
associatedtype Element
/// Notify observer about sequence event.
/// - parameter event: Event that occurred.
func on(_ event: Event<Element>)
}
/// A type-erased `ObserverType`.
/// Forwards operations to an arbitrary underlying observer with the same `Element` type, hiding the specifics of the underlying observer type.
public struct AnyObserver<Element> : ObserverType {
/// Anonymous event handler type.
public typealias EventHandler = (Event<Element>) -> Void
private let observer: EventHandler
/// Construct an instance whose `on(event)` calls `eventHandler(event)`
/// - parameter eventHandler: Event handler that observes sequences events.
public init(eventHandler: @escaping EventHandler) {
self.observer = eventHandler
}
/// Construct an instance whose `on(event)` calls `observer.on(event)`
/// - parameter observer: Observer that receives sequence events.
public init<Observer: ObserverType>(_ observer: Observer) where Observer.Element == Element {
self.observer = observer.on
}
/// Send `event` to this observer.
/// - parameter event: Event instance.
public func on(_ event: Event<Element>) {
return self.observer(event)
}
/// Erases type of observer and returns canonical observer.
/// - returns: type erased observer.
public func asObserver() -> AnyObserver<Element> {
return self
}
}ObserverType
是一个泛型协议,AnyObserver
是一个用于类型擦除的包装类型。AnyObserver
定义了方法指针(闭包),向实现协议的抽象类型实例所声明的方法。同时
AnyObserver
自身又遵循 ObserverType
协议,在调用 AnyObserver
对应的协议时,它会将方法调用转发至对应方法指针所对应的方法。
除了 AnyObserver
之外,Observable
同样也是一个用于类型擦除的包装类型,其工作原理也是基本相似。
此外,swift
标准库中也大量应用了类型擦除,比如:AnySequence
、AnyIterator
、AnyIndex
、AnyHashable
、AnyCollection
等等。后续有时间,我们再来看看标准库中对于泛型协议的类型擦除是怎么做,可以肯定的是,其实现原理基本是一致的
总结
本文,我们通过泛型协议的例子,了解了类型擦除的作用。这里,类型擦除将泛型协议所关联的类型信息进行了擦除,本质上是通过类型参数的方式,让实现抽象类型成员具体化。在面向协议编程中,类型擦除也是一种非常常见的手段,后续我们阅读相关代码时,也就不会对包装类型产生迷惑了。
参考
- Swift: Why Associated Types?
- Swift: Associated Types
- Swift: Associated Types, cont.
- Inception
- Type-erasure in Stdlib
- A Little Respect for AnySequence
- How to use generic protoco as a variable type
- Thunk. Wikipedia
- Thunk 函数的含义和用法
- Swift Generic Protocols
- 当 Swift 中的协议遇到泛型
- 神奇的类型擦除
- Keep Calm and Type Erase On
- Compile Time vs. Run Time Type Checking in Swift
- swift的泛型协议为什么不用
语法 - Swift World: Type Erasure
- MySequece