添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

之前在一些分享会上经常听到 类型擦除 (Type Erase)这个概念,从其命名上大概知道它要干什么,但是对于为什么要用它?以及什么场景下使用它?对此,我并没有深刻的理解。于是,借着假期好好研究了一下。问题的一切要从泛型协议说起。

协议如何支持泛型?

我们知道,在 Swift 中,protocol 支持泛型的方式与 class/struct/enum 不同,具体说来:

  • 对于 class/struct/enum,其采用 类型参数(Type Parameters) 的方式。
  • 对于 protocol,其采用 抽象类型成员(Abstract Type Member) 的方式,具体技术称为 关联类型(Associated Type)
  • 分别如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // class
    class GenericClass<T> { ... }

    // struct
    struct GenericStruct<T> { ... }

    // enum
    enum GenericEnum<T> { ... }

    // protocol
    protocol GenericProtocol {
    associatedtype AbstractType
    func next() -> AbstractType
    }

    这时候我们可能会有一个疑问:为什么 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
    15
    protocol 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)
    从上述代码可以看出,value 既可以表示 Point 类型,又可以表示 Line 类型。事实上,value 的实际类型是编译器生成的一种特殊数据类型 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
    22
    protocol 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
    2
    let 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
    11
    protocol 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    protocol Generator {
    associatedtype AbstractType
    func generate() -> AbstractType
    }

    struct GeneratorThunk<T>: Generator {
    private let _generate: () -> T

    init<G: Generator>(_ gen: G) where G.AbstractType == T {
    _generate = gen.generate
    }

    func generate() -> T {
    return _generate()
    }
    }

    当我们拥有一个 thunk,我们可以把它当做类型使用(需要提供具体类型)。

    1
    2
    3
    4
    5
    6
    7
    8
    struct StringGenerator: Generator {
    typealias AbstractType = String
    func generate() -> String {
    return "zero"
    }
    }

    let gens: GeneratorThunk<String> = GeneratorThunk(StringGenerator())

    采用 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
    29
    protocol 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 5
    在这里, AnyPrinter 并没有显式地引用 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
    41
    public 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
  • to use generic protoco as a variable type
  • Thunk. Wikipedia
  • Thunk 函数的含义和用法
  • Swift Generic Protocols
  • 当 Swift 中的协议遇到泛型
  • 神奇的类型擦除
  • Calm and Type Erase On
  • Compile Time vs. Run Time Type Checking in Swift
  • swift的泛型协议为什么不用 语法
  • Swift World: Type Erasure
  • MySequece
  •