阅读 339

Swift 泛型协议

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

协议如何支持泛型?

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

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

分别如下所示:

// 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 的类型。而抽象类型成员则可以用来实现类型约束的。

如何存储非泛型协议?

下面,我们来看一下协议的存储。首先,我们来考虑非泛型协议。

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 ContainerExistential Container 对具体类型进行封装,从而实现存储一致性。关于 Existential Container 的具体内容,可以参考《Swift性能优化(2)——协议与泛型的实现》

如何存储泛型协议?

接下来,我们再来考虑泛型协议的存储。

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 类型可以保证存储一致性。

事实上,上述代码从表面上看的确不会有问题,但是我们忽略了泛型协议的本质——约束类型。我们可以在上述代码的基础上,继续加上如下代码:

let x = value.generate()
复制代码

由于 Generator 协议约束了 generate() 方法的返回类型,在本例中,x 的类型既可能是 Int,又可能是 String。而 Swift 本身又是一种强类型语言,所有的类型必须在编译时确定。因此,swift 无法直接支持泛型协议的存储。

所以,在实际开发中,Xcode 会对以下这种类型的定义报错。

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),从而骗过编译器,解决该问题的思路有两种:

  • 泛型协议转换成非泛型协议。
  • 泛型协议封装成的具体类型。

对于『泛型协议转换成非泛型协议』,由于泛型协议的实现采用的是抽象类型成员,而不是类型参数,只能基于抽象类型成员进行泛型约束,然而通过转换而来的协议本质上仍然是泛型协议,如下所示。此方法无效。

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 函数。

具体的解决方法是:

  • 定义一个『中间层结构体』,该结构体实现了协议的所有方法。
  • 在『中间层结构体』实现的具体协议方法中,再转发给『实现协议的抽象类型』。
  • 在『中间层结构体』的初始化过程中,『实现协议的抽象类型』会被当做参数传入(依赖注入)。
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,我们可以把它当做类型使用(需要提供具体类型)。

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},如:AnySequenceAnyCollection

下面,是以 Swift 标准库的方式对泛型协议进行类型擦除。

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 指向了 baseprint 方法,通过这种方式,base 被柯里化成了 self,从而隐式地引用了 base 实例。

具体应用

在 RxSwift 中,就有针对泛型协议类型擦除的相关应用,我们来看下面这段代码:

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 标准库中也大量应用了类型擦除,比如:AnySequenceAnyIteratorAnyIndexAnyHashableAnyCollection 等等。后续有时间,我们再来看看标准库中对于泛型协议的类型擦除是怎么做,可以肯定的是,其实现原理基本是一致的

总结

本文,我们通过泛型协议的例子,了解了类型擦除的作用。这里,类型擦除将泛型协议所关联的类型信息进行了擦除,本质上是通过类型参数的方式,让实现抽象类型成员具体化。在面向协议编程中,类型擦除也是一种非常常见的手段,后续我们阅读相关代码时,也就不会对包装类型产生迷惑了。

参考

  1. Swift: Why Associated Types?
  2. Swift: Associated Types
  3. Swift: Associated Types, cont.
  4. Inception
  5. Type-erasure in Stdlib
  6. A Little Respect for AnySequence
  7. How to use generic protoco as a variable type
  8. Thunk. Wikipedia
  9. Thunk 函数的含义和用法
  10. Swift Generic Protocols
  11. 当 Swift 中的协议遇到泛型
  12. 神奇的类型擦除
  13. Keep Calm and Type Erase On
  14. Compile Time vs. Run Time Type Checking in Swift
  15. swift的泛型协议为什么不用语法
  16. Swift World: Type Erasure
  17. MySequece
文章分类
iOS
文章标签