前言
在 Swift 的世界中,如果我們將協議稱之為國王,那么泛型則可以視作皇后,所謂一山不容二虎,當我們把這兩者結合起來使用的時候,似乎會遇到極大的困難。那么是否有一種方法,能夠將這兩個概念結合在一起,以便讓它們成為我們前進道路上的墊腳石,而不是礙手礙腳的呢?答案是有的,這里我們將會使用到類型擦除 (Type Erasure) 這個強大的特性。
你也許曾聽過類型擦除,甚至也使用過標準庫提供的類型擦除類型如 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 標準庫中提供了很多這樣的類型,如 AnyCollection、AnyHashable 及 AnyIndex。這些類型在你自定義泛型或協議的時候非常的管用,你也可以直接使用這些類型來簡化你的代碼。接下來讓我們探索實現類型擦除的多種方式吧。
基于類的類型擦除
有時我們需要在不暴露類型信息的情況下從多個類型中包裝一些公共的功能,這聽起來就像是父類-子類的關系。事實上我們的確可以使用抽象父類來實現類型擦除。父類提供 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()") }}
MAnySequence 對 makeIterator 方法實現也差不多。直接調用將拋出異常,這用來提示子類需要重寫這個方法:
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.Iteratorinit(_ wrapped: Seq.Iterator) { self.wrapped = wrapped}
在 next 方法中調用被包裝的序列迭代器:
override func next() -> Seq.Element? { return wrapped.next() }}
相似地,MAnySequenceImpl 包裝一個序列:
var seq: Seqinit(_ 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() }}
MAnySequence 與 Iterator 很相似:持有一個參數為空返回 Iterator 類型的存儲型屬性。遵循 Sequence 協議并在 makeIterator 方法中調用這個屬性。
let _makeIterator: () -> Iteratorfunc 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 設計了類型擦除類型: KeyedEncodingContainer 和 KeyedDecodingContainer。它們都是容器協議類型包裝器,可用來在不知道底層具體類型信息的情況下實現 Encode 和 Decode。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對VEVB武林網的支持。
作者:Mike Ash,原文鏈接,原文日期:2017-12-18
譯者:rsenjoyer;校對:Yousanflics,numbbbbb;定稿:Forelax
新聞熱點
疑難解答