[譯]Swift 中的型別擦除

CepheusSun發表於2019-03-04

你可能聽過這個術語 :型別擦除。甚至你也用過標準庫中的型別擦除(AnySequence)。但是具體什麼是型別擦除, 我們怎麼才能實現型別擦除呢?這篇文章就是介紹這件事情的。

在日常的開發中, 總有想要把某個類或者是某些實現細節對其他模組隱藏起來, 不然總會感覺這些類在專案裡到處都是。或者想要實現兩個不同類之間的互相轉換。型別擦除就是一個移除某個類的型別標準, 將其變得更加通用的過程。

到這裡很自然的就會想到協議或者是提取抽象的父類來做這件事情。協議或者父類 就可以看作是一種實現型別擦除的方式。舉個例子:

NSString 在標準庫中我們是沒辦法得到 NSString 的例項的,我們得到的所有的 NSString 物件其實都是標準庫中 NSString 的私有子類。這些私有型別對外界可以說是完全隱藏起來了的, 同時可以是用 NSString 的 API 來使用這些例項。所有的子類我們在使用的時候都不需要知道他們具體是什麼, 也就不需要考慮他們具體的型別資訊了。

在處理 Swift 中的泛型和有關聯型別的協議的時候, 就需要一些更高階的東西了。Swift 不允許把協議當作類來使用。如果你想要寫一個接受一個 Int 型別的序列的方法。這麼寫是不對的:

func f(seq: Sequence<Int>) {...}

// Compile error: Cannot specialize non-generic type `Sequence`
複製程式碼

這種情況下, 我們應該考慮使用的是泛型:

func f<S: Sequence>(seq: S) where S.Element == Int { ... }
複製程式碼

這樣寫就可以了。但是, 還是有一些情況是比較麻煩的比如說: 我們無法使用這樣的程式碼來表達返回值型別或者是屬性

func g<S: Sequence>() -> S where S.Element == Int { ... }
複製程式碼

這麼寫並不會是我們想要的那種結果。在這行程式碼中,我們想要的是返回一個滿足條件的類的例項,但是這行程式碼會允許呼叫者去選擇他想要的具體的型別, 然後 g 這個方法去提供合適的值。

protocol Fork {
    associatedtype E
    func call() -> E
}

struct Dog: Fork {
    typealias E = String
    func call() -> String {
        return "?"
    }
}

struct Cat: Fork {
    typealias E = Int
    
    func call() -> Int {
        return 1
    }
}

func g<S: Fork>() -> S where S.E == String {
    return Dog() as! S
}

// 在這裡可以看出來。g 這個函式具體返回什麼東西是在呼叫的時候決定的。就是說要想正確的使用 g 這個函式必須使用  `let dog: Dog = g()`  這樣的程式碼
let dog: Dog = g()
dog.call()

// error
let dog = g()
let cat: Cat = g()
複製程式碼

Swift 提供了 AnySequence 這個類來解決這個問題。AnySequence 包裝了任意的 Sequence 並把他的型別資訊給隱藏起來了。然後通過 AnySequence 來代替這個。有了 AnySequence 我們可以這樣來寫上面的 fg 方法。

func f(seq: AnySequence<Int>) { ... }
func g() -> AnySequence<Int> { ... }
複製程式碼

這麼一來, 泛型沒有了, 而且所有具體的型別資訊都被隱藏起來了。使用 AnySequence 增加了一點點的複雜性和執行成本,但是程式碼卻更乾淨了。

Swift 標準庫中有很多這樣的型別, 比如 AnyCollection, AnyHashable, AnyIndex 等。 在程式碼中你可以自己定義一些泛型或者協議, 或者直接使用這些特性來簡化程式碼。

基於類的擦除

我們需要在不公開型別資訊的情況下從多個型別中包裝出來一些公共的功能。這很自然就能想到抽象父類。事實上我們確實可以通過抽象父類來實現型別擦除。父類暴露 API 出來,子類根據具體的型別資訊來做具體的實現。我們來看看怎麼自己實現一個類似 AnySequence 的東西。

class MAnySequence<Element>: Sequence {
複製程式碼

這個類需要實現 iterator 型別作為 makeIterator 的返回型別。我們必須要做兩次型別擦除來隱藏底層的序列型別以及迭代器的型別。這種內在的迭代器型別遵守了 IteratorProtocol 協議並且在 next() 方法中使用 fatalError 來丟擲異常。Swift 本身是不支援抽象類的, 所以這就足夠了:

    class Iterator: IteratorProtocol {
        func next() -> Element? {
            fatalError("Must override next()")
        }
    }
複製程式碼

ManySequencemakeIterator 方法的實現也差不多, 使用 fatalError 來丟擲異常。 這個錯誤用來提示子類來實現這個功能:

    func makeIterator() -> Iterator {
        fatalError("Must override makeIterator()")
    }
複製程式碼

這就是基於類的型別擦除需要的公共 API。私有的實現需要去子類化這個類。這公共類被元素的型別引數化, 但是私有的實現卻在這個型別當中:

private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<Seq.Element> {
複製程式碼

這個類需要內部的子類來實現上面提到的兩個方法:

class IteratorImpl: Iterator {
複製程式碼

這一步包裝了這個序列的迭代器的型別

    class IteratorImpl: Iterator {
        var wrapped: Seq.Iterator
        
        init(_ wrapped: Seq.Iterator) {
            self.wrapped = wrapped
        }
    }
複製程式碼

這一步實現了 next 方法。 實際上是呼叫它包裝的序列的迭代器的 next 方法.

        override func next() -> Element? {
            return wrapped.next()
        }
複製程式碼

相似的, MAnySequenceImpl 是 sequence 的包裝。

    var seq: Seq
    
    init(_ seq: Seq) {
        self.seq = seq
    }
複製程式碼

這一步實現了 makeIterator 方法。從包裝的序列中去獲取迭代去物件, 然後把這個迭代器物件包裝給 IteratorImpl

    override func makeIterator() -> IteratorImpl {
        return IteratorImpl(seq.makeIterator())
    }
複製程式碼

還需要一點: 使用 MAnySequence 來初始化一個 MAnySequenceImpl,但是返回值還是標記成 MAnySequence 型別。

extension MAnySequence {
    static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where Seq.Element == Element {
        return MAnySequenceImpl<Seq>(seq)
    }
}
複製程式碼

我們來用一下這個 MAnySequence:

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
        print(elt)
    }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence.make(array))
printInts(MAnySequence.make(array[1 ..< 4]))
複製程式碼

基於函式的擦除

**我們希望公開多個型別的功能而不公開這些型別。**很自然的方法是儲存那些簽名只涉及到我們想要公開的型別的函式。函式的主體可以在底層資訊已知的上下文中建立。

我們來看看 MAnySequence 要怎麼來實現呢?更上面的內容差不多。只是這次因為我們不需要繼承而且他只是一個容器,所以我們用 Struct 來實現。

還是宣告一個 Struct

struct MAnySequence<Element>: Sequence {
複製程式碼

跟上面一樣, 實現 Sequence 協議需要有一個迭代器(Iterator)來作為返回值。這個東西也是一個 struct 它有一個儲存屬性, 這個儲存屬性是一個不接受引數, 返回一個Element? 的函式。 他是 IteratorProtocol 這個協議要求的

    struct Iterator: IteratorProtocol {
        let _next: () -> Element?
        
        func next() -> Element? {
            return _next()
        }
    }
複製程式碼

MAnySequence 跟這個也相似。他包含了一個返回 Iterator 的函式的儲存屬性。 Sequence 通過呼叫這個函式來實現。

    let _makeIterator: () -> Iterator
    
    func makeIterator() -> Iterator {
        return _makeIterator()
    }
複製程式碼

MAnySequenceinit 方法是最重要的地方。他接受任意的 Sequence 作為引數(Sequence<Int>Sequence<String>):

init<Seq: Sequence>(_ seq: Seq) where Seq.Element == Element {
複製程式碼

然後需要把這個 Sequence 需要的功能包裝在這個函式中:

        _makeIterator = {
複製程式碼

再然後我們需要在這裡做一個迭代器 Sequence 正好有這個東西:

var iterator = seq.makeIterator()
複製程式碼

最後我們把這個迭代器包裝給 MAnySequence。 他的 _next 函式就能呼叫到 iteratornext 函式了:

            return Iterator(_next: { iterator.next() })
        }
    }
}
複製程式碼

下面看這個 MAnySequence 是怎麼用的:

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
        print(elt)
    }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence(array))
printInts(MAnySequence(array[1 ..< 4]))
複製程式碼

搞定!

這種基於函式的擦除方法在處理需要把一小部分功能作為更大型別的一部分來包裝的時候非常有效, 這樣做就不需要有單獨的類來擦除其他類的型別資訊。

比如說,我們需要寫一些能在特定幾個集合型別上面使用的程式碼:

class GenericDataSource<Element> {
    let count: () -> Int
    
    let getElement: (Int) -> Element
    
    init<C: Collection>(_ c: C) where C.Element == Element, C.Index == Int {
        count = { Int(c.count) }
        getElement = { c[$0 - c.startIndex]}
    }
}
複製程式碼

這樣, GenericDataSource 中的其他程式碼就能夠直接使用 count()getElement() 兩個方法來操作傳入的collection 了。並且這個集合型別不會汙染 GenericDataSource 的泛型引數。

總結

型別擦除是個非常有用的技術。他被用來阻止泛型對程式碼的侵入, 也能夠讓介面更加的簡單。通過將底層的型別資訊包裝起來, 將 API 和具體的功能分開。使用靜態的公有型別或者將 API 包裝進函式都能夠做到型別擦除。基於函式做型別擦除對那種只需要幾個功能的簡單情況尤其有用。

Swift 標準庫提供了一些可以直接使用的型別擦除。AnySequenceSequence 的包裝, 從名字可以看出來, 他允許你在不知道具體型別的情況下迭代遍歷某個序列。AnyIterator 是他的好朋友, 它提供了一個型別已經被擦除掉的迭代器。AnyHashable 包裝了型別擦除掉了的 Hashable 型別。Swift 中還有一些基於集合型別的協議。在文件中搜尋 “Any” 就可以看到。標準庫中的 Codable 也有用到了型別擦除: KeyedEncodingContainerKeyedDecodingContainer 都是對應協議型別擦除的包裝。他們用來在不知道具體型別資訊的情況下實現 encode 還有 decode。

最後

前幾天看到 MikeAsh 最新的 Friday Q&A Type Erasure in Swift。想趁著最近沒什麼事情翻譯一下的。結果最近一直沉迷吃雞, 沒有時間去做這件事情。所以…

相關文章