之前在一些分享會上經常聽到 類型擦除(Type Erase)這個概念迅箩,從其命名上大概知道它要干什么吆倦,但是對于為什么要用它育特?以及什么場景下使用它?對此坛怪,我并沒有深刻的理解葡盗。于是尘盼,借著假期好好研究了一下偷办。問題的一切要從泛型協(xié)議說起。
協(xié)議如何支持泛型匆骗?
我們知道劳景,在 Swift 中,protocol 支持泛型的方式與 class/struct/enum 不同碉就,具體說來:
- 對于 class/struct/enum盟广,其采用 類型參數(shù)(Type Parameters) 的方式。
- 對于 protocol瓮钥,其采用 抽象類型成員(Abstract Type Member) 的方式筋量,具體技術稱為 關聯(lián)類型(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 使用泛型參數(shù)碉熄,而 protocol 則使用抽象類型成員桨武?我查閱了很多討論,原因可以歸納為兩點:
- 采用類型參數(shù)的泛型其實是定義了整個類型家族锈津,我們可以通過傳入類型參數(shù)可以轉換成具體類型(類似于函數(shù)調用時傳入不同參數(shù))呀酸,如:
Array<Int>
,Array<String>
琼梆,很顯然類型參數(shù)適用于多次表達性誉。然而窿吩,協(xié)議的表達是一次性的,我們只會實現(xiàn)GenericProtocol
艾栋,而不會特定地實現(xiàn)GenericProtocol<Int>
或GenericProtocol<String>
。 - 協(xié)議在 Swift 中有兩個目的蛉顽,第一個目的是 用來實現(xiàn)多繼承(Swift 語言被設計成單繼承)蝗砾,第二個目的是 強制實現(xiàn)者必須遵守協(xié)議所指定的泛型約束。很明顯携冤,協(xié)議并不是用來表示某種類型悼粮,而是用來約束某種類型,比如:
GenericProtocol
約束了next()
方法的返回類型曾棕,而不是定義GenericProtocol
的類型扣猫。而抽象類型成員則可以用來實現(xiàn)類型約束的。
如何存儲非泛型協(xié)議翘地?
下面申尤,我們來看一下協(xié)議的存儲。首先衙耕,我們來考慮非泛型協(xié)議昧穿。
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 的實際類型是編譯器生成的一種特殊數(shù)據(jù)類型 Existential Container
厅瞎。Existential Container
對具體類型進行封裝饰潜,從而實現(xiàn)存儲一致性。關于 Existential Container 的具體內容和簸,可以參考《Swift性能優(yōu)化(2)——協(xié)議與泛型的實現(xiàn)》彭雾。
如何存儲泛型協(xié)議?
接下來锁保,我們再來考慮泛型協(xié)議的存儲冠跷。
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()
通過非泛型協(xié)議的例子,我們理所當然會覺得上述代碼沒有問題身诺,因為有 Existential Container
類型可以保證存儲一致性蜜托。
事實上,上述代碼從表面上看的確不會有問題霉赡,但是我們忽略了泛型協(xié)議的本質——約束類型橄务。我們可以在上述代碼的基礎上,繼續(xù)加上如下代碼:
let x = value.generate()
由于 Generator
協(xié)議約束了 generate()
方法的返回類型穴亏,在本例中蜂挪,x
的類型既可能是 Int
重挑,又可能是 String
。而 Swift 本身又是一種強類型語言棠涮,所有的類型必須在編譯時確定谬哀。因此,swift 無法直接支持泛型協(xié)議的存儲严肪。
所以史煎,在實際開發(fā)中,Xcode 會對以下這種類型的定義報錯驳糯。
let value: Generator = IntGenerator()
// Error: Protocol 'Generator' can only be used as a generic constraint because it has Self or associated type requirements
那么篇梭,如何解決泛型協(xié)議的存儲呢?
解決方法
問題的本質是要將泛型協(xié)議的所約束的類型進行擦除酝枢,即 類型擦除 (Type Erase)恬偷,從而騙過編譯器,解決該問題的思路有兩種:
- 泛型協(xié)議轉換成非泛型協(xié)議帘睦。
- 泛型協(xié)議封裝成的具體類型袍患。
對于『泛型協(xié)議轉換成非泛型協(xié)議』,由于泛型協(xié)議的實現(xiàn)采用的是抽象類型成員竣付,而不是類型參數(shù)协怒,只能基于抽象類型成員進行泛型約束,然而通過轉換而來的協(xié)議本質上仍然是泛型協(xié)議卑笨,如下所示孕暇。此方法無效。
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
對于『泛型協(xié)議封裝成的具體類型』赤兴,事實上妖滔,這是業(yè)界普遍的解決方案,swift 中很多系統(tǒng)庫都是采用這種思路來解決的桶良。
為此座舍,我們可以使用 thunk 技術來解決。什么是 thunk陨帆?一個 thunk 通常是一個子程序曲秉,它被創(chuàng)造出來,用于協(xié)助調用其他的子程序疲牵。說到底承二,就是通過創(chuàng)造一個中間層來解決遇到的問題。
thunk 技術應用非常廣泛纲爸,比如:oc swift 混編時亥鸠,我們可以在調用棧中看到存在 thunk 函數(shù)。
具體的解決方法是:
- 定義一個『中間層結構體』,該結構體實現(xiàn)了協(xié)議的所有方法负蚊。
- 在『中間層結構體』實現(xiàn)的具體協(xié)議方法中神妹,再轉發(fā)給『實現(xiàn)協(xié)議的抽象類型』。
- 在『中間層結構體』的初始化過程中家妆,『實現(xiàn)協(xié)議的抽象類型』會被當做參數(shù)傳入(依賴注入)鸵荠。
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 技術蛹找,我們把泛型協(xié)議封裝成的具體類型,其本質就是對泛型協(xié)議進行了 類型擦除(Type Erase)塑荒,從而解決了泛型類型的存儲問題熄赡。
類型擦除
關于類型擦除姜挺,在 Swift 標準庫的實現(xiàn)中齿税,一般會創(chuàng)建一個包裝類型(class 或 struct)將遵循了協(xié)議的對象進行封裝。包裝類型本身也遵循協(xié)議炊豪,它會將對協(xié)議方法的調用傳遞到內部的對象中凌箕。包裝類型一般命名為 Any{protocol-name}
,如:AnySequence
词渤、AnyCollection
牵舱。
下面,是以 Swift 標準庫的方式對泛型協(xié)議進行類型擦除缺虐。
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 中饱普,就有針對泛型協(xié)議類型擦除的相關應用,我們來看下面這段代碼:
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
是一個泛型協(xié)議状共,AnyObserver
是一個用于類型擦除的包裝類型套耕。AnyObserver
定義了方法指針(閉包),向實現(xiàn)協(xié)議的抽象類型實例所聲明的方法峡继。同時 AnyObserver
自身又遵循 ObserverType
協(xié)議箍铲,在調用 AnyObserver
對應的協(xié)議時,它會將方法調用轉發(fā)至對應方法指針所對應的方法鬓椭。
除了 AnyObserver
之外颠猴,Observable
同樣也是一個用于類型擦除的包裝類型关划,其工作原理也是基本相似。
此外翘瓮,swift 標準庫中也大量應用了類型擦除贮折,比如:AnySequence
、AnyIterator
资盅、AnyIndex
调榄、AnyHashable
、AnyCollection
等等呵扛。后續(xù)有時間每庆,我們再來看看標準庫中對于泛型協(xié)議的類型擦除是怎么做,可以肯定的是今穿,其實現(xiàn)原理基本是一致的
總結
本文缤灵,我們通過泛型協(xié)議的例子,了解了類型擦除的作用蓝晒。這里腮出,類型擦除將泛型協(xié)議所關聯(lián)的類型信息進行了擦除,本質上是通過類型參數(shù)的方式芝薇,讓實現(xiàn)抽象類型成員具體化胚嘲。在面向協(xié)議編程中,類型擦除也是一種非常常見的手段洛二,后續(xù)我們閱讀相關代碼時馋劈,也就不會對包裝類型產生迷惑了。
參考
- 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 函數(shù)的含義和用法
- Swift Generic Protocols
- 當 Swift 中的協(xié)議遇到泛型
- 神奇的類型擦除
- Keep Calm and Type Erase On
- Compile Time vs. Run Time Type Checking in Swift
- swift的泛型協(xié)議為什么不用<T>語法
- Swift World: Type Erasure
- MySequece