Swift 泛型協議

語言: CN / TW / HK

之前在一些分享會上經常聽到 型別擦除 (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. MySequence