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)——协议与泛型的实现》

image

如何存储泛型协议?

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

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

image

具体的解决方法是:

  • 定义一个『中间层结构体』,该结构体实现了协议的所有方法。
  • 在『中间层结构体』实现的具体协议方法中,再转发给『实现协议的抽象类型』。
  • 在『中间层结构体』的初始化过程中,『实现协议的抽象类型』会被当做参数传入(依赖注入)。
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的泛型协议为什么不用<T>语法
  16. Swift World: Type Erasure
  17. MySequece
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容