Swift 型別擦除

SwiftGG翻譯組發表於2018-10-11

作者:Mike Ash原文連結,原文日期:2017-12-18 譯者:rsenjoyer;校對:Yousanflicsnumbbbbb;定稿:Forelax

你也許曾聽過型別擦除,甚至也使用過標準庫提供的型別擦除型別如 AnySequence。但到底什麼是型別擦除? 如何自定義型別擦除? 在這篇文章中,我將討論如何使用型別擦除以及如何自定義。在此感謝 Lorenzo Boaro 提出這個主題。

前言

有時你想對外部呼叫者隱藏某個類的具體型別,或是一些實現細節。在一些情況下,這樣做能防止靜態型別在專案中濫用,或者保證了型別間的互動。型別擦除就是移除某個類的具體型別使其變得更通用的過程。

協議或抽象父類可作為型別擦除簡單的實現方式之一。例如 NSString 就是一個例子,每次建立一個 NSString 例項時,這個物件並不是一個普通的 NSString 物件,它通常是某個具體的子類的例項,這個子類一般是私有的,同時這些細節通常是被隱藏起來的。你可以使用子類提供的功能而不用知道它具體的型別,你也沒必要將你的程式碼與它們的具體型別聯絡起來。

在處理 Swift 泛型以及關聯型別協議的時候,可能需要使用一些高階的內容。Swift 不允許把協議當做具體的型別來使用。例如, 如果你想編寫一個方法,他的引數是一個包含了Int 的序列,那麼下面這種做法是不正確的:

    func f(seq: Sequence<Int>) { ...
複製程式碼

你不能這樣使用協議型別,這樣會在編譯時報錯。但你可以使用泛型來替代協議, 解決這個問題:

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

有時候這樣寫完全可以,但有些地方還存在一些比較麻煩的情況,通常你不可能只在一個地方新增泛型: 一個泛型函式對其他泛型要求更多... 更糟糕的是,你不能將泛型作為返回值或者屬性。這就跟我們想的有點不一樣了。

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

我們希望函式 g 能返回任何符合的型別,但上面這個不同,它允許呼叫者選擇他所需要的型別,然後函式 g 來提供一個合適的值。

Swift 標準庫中提供了 AnySequence 來幫助我們解決這個問題。AnySequence 包裝了一個任意型別的序列,並擦除了它的型別。使用 AnySequence 來訪問這個序列,我們來重寫一下函式 f 與 函式 g

    func f(seq: AnySequence<Int>) { ...

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

泛型部分不見了,同時具體的型別也被隱藏起來了。由於使用了 AnySequence 包裝具體的值,它帶來了一定的程式碼複雜性以及執行時間成本。但是程式碼卻更簡潔了。

Swift 標準庫中提供了很多這樣的型別,如 AnyCollectionAnyHashableAnyIndex。這些型別在你自定義泛型或協議的時候非常的管用,你也可以直接使用這些型別來簡化你的程式碼。接下來讓我們探索實現型別擦除的多種方式吧。

基於類的型別擦除

有時我們需要在不暴露型別資訊的情況下從多個型別中包裝一些公共的功能,這聽起來就像是父類-子類的關係。事實上我們的確可以使用抽象父類來實現型別擦除。父類提供 API 介面,不用去管誰來實現。而子類根據具體的型別資訊實現相應的功能。

接下來我們將使用這種方式來自定義 AnySequence,我們將其命名為 MAnySequence

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

這個類需要一個 iterator 型別作為 makeIterator 返回型別。我們必須要做兩次型別擦除來隱藏底層的序列型別以及迭代器的型別。我們在 MAnySequence 內部定義了一個 Iterator 類,該類遵循著 IteratorProtocol 協議,並在 next() 方法中使用 fatalError 丟擲異常。Swift 本身不支援抽象型別,但這樣也夠了:

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

MAnySequencemakeIterator 方法實現也差不多。直接呼叫將丟擲異常,這用來提示子類需要重寫這個方法:

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

這樣就定義了一個基於類的型別擦除的API,私有的子類將來實現這些API。公共類通過元素型別引數化,但私有實現類由它包裝的序列型別進行引數化:

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

MAnySequenceImpl 需要一個繼承於 Iterator 的子類:

        class IteratorImpl: Iterator {
複製程式碼

IteratorImpl 包裝了序列的迭代器:

            var wrapped: Seq.Iterator

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

next 方法中呼叫被包裝的序列迭代器:

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

相似地,MAnySequenceImpl 包裝一個序列:

        var seq: Seq

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

從序列中獲取迭代器,然後將迭代器包裝成 IteratorImpl 物件返回,這樣就實現了 makeIterator 的功能。

        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 提供一個初始化方法。

我們來試試 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 MAnySequence<Element>: Sequence {
複製程式碼

跟之前一樣,MAnySequence 也需要一個可返回的迭代器(Iterator)。迭代器同樣被設計為結構體,並持有一個引數為空並返回 Element? 的儲存型屬性,實際上這個屬性是一個函式,被用於 IteratorProtocol 協議的 next 方法中。接下來 Iterator 遵循 IteratorProtocol 協議,並在 next 方法中呼叫函式:

        struct Iterator: IteratorProtocol {
            let _next: () -> Element?

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

MAnySequenceIterator 很相似: 持有一個引數為空返回 Iterator 型別的儲存型屬性。遵循 Sequence 協議並在 makeIterator 方法中呼叫這個屬性。

        let _makeIterator: () -> Iterator

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

MAnySequence 的建構函式正是魔法起作用的地方,它接收任意序列作為引數:

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

接下來需要在建構函式中包裝此序列的功能:

            _makeIterator = {
複製程式碼

如何生成迭代器?請求 Seq 序列生成:

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

接下來我們利用自定義的迭代結構體包裝序列生成的迭代器,包裝後的 _next 屬性將會在迭代器協議的 next() 方法中被呼叫:

                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]))
複製程式碼

正確執行,太棒了!

當需要將小部分功能包裝為更大型別的一部分時,這種基於函式的型別擦除方法特別實用,這樣做就不需要有單獨的類來實現被擦除型別的這部分功能了。

比方說你現在想要編寫一些適用於各種集合型別的程式碼,但它真正需要能夠對這些集合執行的操作是獲取計數並執行從零開始的整數下標。如訪問 tableView 資料來源。它可能看起來像這樣:

    class GenericDataSource<Element> {
        let count: () -> Int
        let getElement: (Int) -> Element

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

GenericDataSource 其他程式碼可通過呼叫 count()getElement() 來操作傳入的集合。且不會讓集合型別破壞 GenericDataSource 泛型引數。

結束語

型別擦除是一種非常有用的技術,它可用來阻止泛型對程式碼的侵入,也可用來保證介面簡單明瞭。通過將底層型別包裝起來,將API與具體的功能進行拆分。這可以通過使用抽象的公共超類和私有子類或將 API 包裝在函式中來實現。對於只需要一些功能的簡單情況,基於函式型別擦除極其有效。

Swift 標準庫提供了幾種可直接利用的型別擦除型別。如 AnySequence 包裝一個 Sequence,正如其名,AnySequence 允許你對序列迭代而無需知道序列具體的型別。AnyIterator 也是型別擦除的型別,它提供一個型別擦除的迭代器。AnyHashable 也同樣是型別擦除的型別,它提供了對Hashable型別訪問功能。Swift 還有很多基於集合的擦除型別,你可以通過搜尋 Any 來查閱。標準庫中也為 Codable API 設計了型別擦除型別: KeyedEncodingContainerKeyedDecodingContainer。它們都是容器協議型別包裝器,可用來在不知道底層具體型別資訊的情況下實現 EncodeDecode

這就是今天全部的內容了,下次再見。你們的建議對 Friday Q&A 是最好的鼓勵,所以如果你關於這個主題有什麼好的想法,請 發郵件到這裡

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

相關文章