Swift 型別擦除

語言: CN / TW / HK

《Swift 泛型協議》 中,我們探討了如何基於型別擦除技術解決 Swift 泛型協議的儲存問題,通過定義一個型別擦除包裝器 AnyPrinter 解決了泛型協議 Printer 的儲存問題。但是,AnyPrinter 並沒有顯式地引用 base 例項,因為當我們定義一個泛型型別的屬性時,編譯器會報錯。

如果我們在 AnyPrinter 中定義一個 base 屬性用於顯式引用例項。當我們將 base 宣告為 Printer,編譯器會報錯:Cannot specialize non-generic type 'Printer';當我們將 base 宣告為 Printer<T>,編譯器會報錯:Protocol 'Printer' can only be used as a generic constraint because it has Self or associated type requirements。如下所示。

```swift struct AnyPrinter: Printer { typealias T = U

var base: Printer<T>
// Error: Protocol 'Printer' can only be used as a generic constraint because it has Self or associated type requirements

var base: Printer
// Error: Cannot specialize non-generic type 'Printer'

init<Base: Printer>(base : Base) where Base.T == U {
    self.base = base
}

func print(val: T) {
    base.print(val)
}

} ```

最終我們基於方法指標隱式地引用了 base 例項。如下所示。

```swift struct AnyPrinter: 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)
}

} ```

本文,我們就來探討一下,在泛型協議中,如何顯式地引用 base 例項。

swift protocol Printer { associatedtype T func print(val: T) }

中間型別

上述實現中,在 AnyPrinter 中定義了一個 base 屬性,在宣告其型別時,無論是宣告為 Printer<T> 還是 Printer,編譯器都會報錯。為了解決這個問題,我們還是以那句經典名言為指導思想,實現一個包裝型別作為 base 屬性的型別

這裡我們需要另外定義兩個型別,兩者是基類和子類的關係,並且都遵循泛型協議 Printer。至於為什麼定義兩個型別,我們後面再解釋。在 Swift 標準庫實現中,經常使用 box 命名中間型別,或者說是盒子型別、包裝型別,這裡我們同樣以 box 進行命名。

Box 基類

如下所示為 box 基類的實現,由於泛型型別 _AnyPrinterBoxBase 遵循了 Printer 泛型協議,型別引數會自動繫結至關聯型別。在真正使用時,_AnyPrinterBoxBase 並不會保持抽象,它最終會被繫結到某個特定型別。 ```swift class _AnyPrinterBoxBase: Printer { typealias T = E

func print(val: E) {
    fatalError()
}

} ```

Box 子類

如下所示為 box 子類的實現,其內部封裝了一個例項 var base: Base,並且將方法傳遞給了例項。這個 base 例項才是 Printer 協議真正的實現者。在 _PrinterBox 型別宣告的第一行中,其自動將 Base.TPrinter 協議的關聯型別)繫結為 _AnyPrinterBoxBase.T_AnyPrinterBoxBase 的型別引數) 。此時,我們也無需再在 _PrinterBox 內部通過 typealias T == xxx 的方式手動進行型別繫結。

```swift class _PrinterBox: _AnyPrinterBoxBase { var base: Base

init(_ base: Base) {
    self.base = base
}

override func print(val: Base.T) {
    base.print(val: val)
}

} ```

型別擦除

在實現了中間型別後,我們再來修改型別擦除包裝器 AnyPrinter 的內部實現。具體如下所示,由於我們使用中間型別 box 對 base 進行了封裝,所以這裡我們需要將 AnyPrinter 中的 base 的命名修改為 _box。當我們呼叫 print 方法時,其內部會將 print 方法轉發至 _box,而 _box 內部又會將 print 轉發至 base 這個真正的實現者。

```swift struct AnyPrinter: Printer { var _box: _AnyPrinterBoxBase

init<Base: Printer>(_ base: Base) where Base.T == T {
    _box = _PrinterBox(base)
}

func print(val: T) {
    _box.print(val: val)
}

} ```

現在,我們再來看前文留下的問題:為什麼中間層需要定義基類和子類兩個型別。事實上一開始,我嘗試只定義一個 box 型別 _PrinterBox,如下所示,但是編譯器會報錯: ```swift class _PrinterBox: Printer { typalias T = Base var base: Base

init<Base: Printer>(_ base: Base) where Base.T == T {
    self.base = base
    // Error: Cannot assign value of type 'Base' to type 'Base'
}

func print(val: Base) {

}

} `` 這個報錯看上去有點奇怪,我猜測其原因:雖然構造器通過where Base.T == TBase型別進行了約束,但是卻並沒有將Printer.T繫結至Base型別。不過奇怪的是,我加了typealias T = Base也不管用。如果有人知道原因,可以留言告訴我。最終的解決方案是,實現了兩個基類和子類兩個型別,通過子類的宣告對Printer.T` 進行型別繫結。

最後,我們再來簡單對比一下型別擦除的兩種方案。如下所示,分別是隱式引用 base 和顯式引用 base。其中,Logger 才是 Printer 協議真正的實現者。

具體應用

Codable 原始碼大量使用了面向協議程式設計,為了解決泛型協議的儲存,其也採用了與上述類似的型別擦除方案。如下所示分別是 Codable 中編解碼的核心設計實現,裡面涉及到非常多的類,本質上還是在解決泛型擦除。其中,_KeyedEncodingContainerBox_KeyedDecodingContainerBox 中對於 base 的命名有所不同,這裡命名成了 concrete。另外,__JSONKeyedEncodingContainer__JSONKeyedDecodingContainer 雖然分別是 KeyedEncodingContainerProcotolKeyedDecodingContainerProtocol 的真正實現者,但是它們內部各自將具體的編碼和解碼細節轉交給了 __JSONEncoder__JSONDecoder

總結

事實上,曾經我也嘗試閱讀過 Codable 原始碼,當時對 Swift 型別擦除並不太瞭解,從而導致我根本讀不懂 Codable 的原始碼在幹什麼,為什麼要有這麼多的類進行方法轉發。如今,在瞭解了 Swift 型別擦除之後,Codable 的設計架構一下子就清晰了,後續有時間我們再來探討一下 Codable 的原始碼實現。

總而言之,只有深入瞭解了 Swift 型別擦除後,我們才能領會面向協議程式設計的精髓以及相關設計理念。