Swift 泛型協(xié)議

原文鏈接

之前在一些分享會上經常聽到 類型擦除(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)》彭雾。

image

如何存儲泛型協(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ù)。

image

具體的解決方法是:

  • 定義一個『中間層結構體』,該結構體實現(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 指向了 baseprint 方法剪芍,通過這種方式塞淹,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 標準庫中也大量應用了類型擦除贮折,比如:AnySequenceAnyIterator资盅、AnyIndex调榄、AnyHashableAnyCollection 等等呵扛。后續(xù)有時間每庆,我們再來看看標準庫中對于泛型協(xié)議的類型擦除是怎么做,可以肯定的是今穿,其實現(xiàn)原理基本是一致的

總結

本文缤灵,我們通過泛型協(xié)議的例子,了解了類型擦除的作用蓝晒。這里腮出,類型擦除將泛型協(xié)議所關聯(lián)的類型信息進行了擦除,本質上是通過類型參數(shù)的方式芝薇,讓實現(xiàn)抽象類型成員具體化胚嘲。在面向協(xié)議編程中,類型擦除也是一種非常常見的手段洛二,后續(xù)我們閱讀相關代碼時馋劈,也就不會對包裝類型產生迷惑了。

參考

  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 函數(shù)的含義和用法
  10. Swift Generic Protocols
  11. 當 Swift 中的協(xié)議遇到泛型
  12. 神奇的類型擦除
  13. Keep Calm and Type Erase On
  14. Compile Time vs. Run Time Type Checking in Swift
  15. swift的泛型協(xié)議為什么不用<T>語法
  16. Swift World: Type Erasure
  17. MySequece
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末晾嘶,一起剝皮案震驚了整個濱河市妓雾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌变擒,老刑警劉巖君珠,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異娇斑,居然都是意外死亡策添,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門毫缆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唯竹,“玉大人,你說我怎么就攤上這事苦丁〗牵” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長产上。 經常有香客問我棵磷,道長,這世上最難降的妖魔是什么晋涣? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任仪媒,我火速辦了婚禮,結果婚禮上谢鹊,老公的妹妹穿的比我還像新娘算吩。我一直安慰自己,他們只是感情好佃扼,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布偎巢。 她就那樣靜靜地躺著,像睡著了一般兼耀。 火紅的嫁衣襯著肌膚如雪压昼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天翠订,我揣著相機與錄音巢音,去河邊找鬼遵倦。 笑死尽超,一個胖子當著我的面吹牛,可吹牛的內容都是我干的梧躺。 我是一名探鬼主播似谁,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼掠哥!你這毒婦竟也來了巩踏?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤续搀,失蹤者是張志新(化名)和其女友劉穎塞琼,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體禁舷,經...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡彪杉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了牵咙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片派近。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖洁桌,靈堂內的尸體忽然破棺而出渴丸,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布谱轨,位于F島的核電站戒幔,受9級特大地震影響,放射性物質發(fā)生泄漏土童。R本人自食惡果不足惜溪食,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娜扇。 院中可真熱鬧错沃,春花似錦、人聲如沸雀瓢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刃麸。三九已至醒叁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泊业,已是汗流浹背把沼。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吁伺,地道東北人饮睬。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像篮奄,于是被迫代替她去往敵國和親捆愁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容