你可能聽過這個術(shù)語 :類型擦除为黎。甚至你也用過標(biāo)準(zhǔn)庫中的類型擦除(AnySequence
)庙洼。但是具體什么是類型擦除, 我們怎么才能實現(xiàn)類型擦除呢?這篇文章就是介紹這件事情的畔咧。
在日常的開發(fā)中, 總有想要把某個類或者是某些實現(xiàn)細(xì)節(jié)對其他模塊隱藏起來, 不然總會感覺這些類在項目里到處都是娶靡∧晾危或者想要實現(xiàn)兩個不同類之間的互相轉(zhuǎn)換。類型擦除就是一個移除某個類的類型標(biāo)準(zhǔn)姿锭, 將其變得更加通用的過程塔鳍。
到這里很自然的就會想到協(xié)議或者是提取抽象的父類來做這件事情。協(xié)議或者父類 就可以看作是一種實現(xiàn)類型擦除的方式呻此。舉個例子:
NSString 在標(biāo)準(zhǔn)庫中我們是沒辦法得到 NSString
的實例的轮纫,我們得到的所有的 NSString
對象其實都是標(biāo)準(zhǔn)庫中 NSString
的私有子類。這些私有類型對外界可以說是完全隱藏起來了的, 同時可以是用 NSString
的 API 來使用這些實例焚鲜。所有的子類我們在使用的時候都不需要知道他們具體是什么, 也就不需要考慮他們具體的類型信息了掌唾。
在處理 Swift 中的泛型和有關(guān)聯(lián)類型的協(xié)議的時候, 就需要一些更高級的東西了放前。Swift 不允許把協(xié)議當(dāng)作類來使用。如果你想要寫一個接受一個 Int 類型的序列的方法糯彬。這么寫是不對的:
func f(seq: Sequence<Int>) {...}
// Compile error: Cannot specialize non-generic type 'Sequence'
這種情況下, 我們應(yīng)該考慮使用的是泛型:
func f<S: Sequence>(seq: S) where S.Element == Int { ... }
這樣寫就可以了凭语。但是, 還是有一些情況是比較麻煩的比如說: 我們無法使用這樣的代碼來表達(dá)返回值類型或者是屬性。
func g<S: Sequence>() -> S where S.Element == Int { ... }
這么寫并不會是我們想要的那種結(jié)果撩扒。在這行代碼中似扔,我們想要的是返回一個滿足條件的類的實例,但是這行代碼會允許調(diào)用者去選擇他想要的具體的類型, 然后 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 這個函數(shù)具體返回什么東西是在調(diào)用的時候決定的。就是說要想正確的使用 g 這個函數(shù)必須使用 `let dog: Dog = g()` 這樣的代碼
let dog: Dog = g()
dog.call()
// error
let dog = g()
let cat: Cat = g()
Swift 提供了 AnySequence 這個類來解決這個問題泉手。AnySequence 包裝了任意的 Sequence 并把他的類型信息給隱藏起來了黔寇。然后通過 AnySequence 來代替這個。有了 AnySequence 我們可以這樣來寫上面的 f
和 g
方法斩萌。
func f(seq: AnySequence<Int>) { ... }
func g() -> AnySequence<Int> { ... }
這么一來缝裤, 泛型沒有了, 而且所有具體的類型信息都被隱藏起來了术裸。使用 AnySequence 增加了一點點的復(fù)雜性和運行成本倘是,但是代碼卻更干凈了亭枷。
Swift 標(biāo)準(zhǔn)庫中有很多這樣的類型, 比如 AnyCollection
, AnyHashable
, AnyIndex
等袭艺。 在代碼中你可以自己定義一些泛型或者協(xié)議, 或者直接使用這些特性來簡化代碼。
基于類的擦除
我們需要在不公開類型信息的情況下從多個類型中包裝出來一些公共的功能叨粘。這很自然就能想到抽象父類猾编。事實上我們確實可以通過抽象父類來實現(xiàn)類型擦除。父類暴露 API 出來升敲,子類根據(jù)具體的類型信息來做具體的實現(xiàn)答倡。我們來看看怎么自己實現(xiàn)一個類似 AnySequence 的東西。
class MAnySequence<Element>: Sequence {
這個類需要實現(xiàn) iterator
類型作為 makeIterator
的返回類型驴党。我們必須要做兩次類型擦除來隱藏底層的序列類型以及迭代器的類型瘪撇。這種內(nèi)在的迭代器類型遵守了 IteratorProtocol
協(xié)議并且在 next()
方法中使用 fatalError
來拋出異常。Swift 本身是不支持抽象類的港庄, 所以這就足夠了:
class Iterator: IteratorProtocol {
func next() -> Element? {
fatalError("Must override next()")
}
}
ManySequence
對 makeIterator
方法的實現(xiàn)也差不多倔既, 使用 fatalError
來拋出異常。 這個錯誤用來提示子類來實現(xiàn)這個功能:
func makeIterator() -> Iterator {
fatalError("Must override makeIterator()")
}
這就是基于類的類型擦除需要的公共 API鹏氧。私有的實現(xiàn)需要去子類化這個類渤涌。這公共類被元素的類型參數(shù)化, 但是私有的實現(xiàn)卻在這個類型當(dāng)中:
private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<Seq.Element> {
這個類需要內(nèi)部的子類來實現(xiàn)上面提到的兩個方法:
class IteratorImpl: Iterator {
這一步包裝了這個序列的迭代器的類型
class IteratorImpl: Iterator {
var wrapped: Seq.Iterator
init(_ wrapped: Seq.Iterator) {
self.wrapped = wrapped
}
}
這一步實現(xiàn)了 next
方法。 實際上是調(diào)用它包裝的序列的迭代器的 next
方法.
override func next() -> Element? {
return wrapped.next()
}
相似的把还, MAnySequenceImpl
是 sequence 的包裝实蓬。
var seq: Seq
init(_ seq: Seq) {
self.seq = seq
}
這一步實現(xiàn)了 makeIterator
方法茸俭。從包裝的序列中去獲取迭代去對象, 然后把這個迭代器對象包裝給 IteratorImpl
。
override func makeIterator() -> IteratorImpl {
return IteratorImpl(seq.makeIterator())
}
還需要一點: 使用 MAnySequence
來初始化一個 MAnySequenceImpl
安皱,但是返回值還是標(biāo)記成 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]))
基于函數(shù)的擦除
我們希望公開多個類型的功能而不公開這些類型。很自然的方法是儲存那些簽名只涉及到我們想要公開的類型的函數(shù)酌伊。函數(shù)的主體可以在底層信息已知的上下文中創(chuàng)建袖迎。
我們來看看 MAnySequence 要怎么來實現(xiàn)呢?更上面的內(nèi)容差不多腺晾。只是這次因為我們不需要繼承而且他只是一個容器燕锥,所以我們用 Struct 來實現(xiàn)。
還是聲明一個 Struct
struct MAnySequence<Element>: Sequence {
跟上面一樣, 實現(xiàn) Sequence 協(xié)議需要有一個迭代器(Iterator)來作為返回值悯蝉。這個東西也是一個 struct
它有一個儲存屬性, 這個儲存屬性是一個不接受參數(shù)归形, 返回一個Element?
的函數(shù)。 他是 IteratorProtocol
這個協(xié)議要求的
struct Iterator: IteratorProtocol {
let _next: () -> Element?
func next() -> Element? {
return _next()
}
}
MAnySequence
跟這個也相似鼻由。他包含了一個返回 Iterator
的函數(shù)的儲存屬性暇榴。 Sequence 通過調(diào)用這個函數(shù)來實現(xiàn)。
let _makeIterator: () -> Iterator
func makeIterator() -> Iterator {
return _makeIterator()
}
MAnySequence
的 init
方法是最重要的地方蕉世。他接受任意的 Sequence
作為參數(shù)(Sequence<Int>
蔼紧、Sequence<String>
):
init<Seq: Sequence>(_ seq: Seq) where Seq.Element == Element {
然后需要把這個 Sequence 需要的功能包裝在這個函數(shù)中:
_makeIterator = {
再然后我們需要在這里做一個迭代器 Sequence
正好有這個東西:
var iterator = seq.makeIterator()
最后我們把這個迭代器包裝給 MAnySequence
。 他的 _next
函數(shù)就能調(diào)用到 iterator
的 next
函數(shù)了:
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]))
搞定狠轻!
這種基于函數(shù)的擦除方法在處理需要把一小部分功能作為更大類型的一部分來包裝的時候非常有效, 這樣做就不需要有單獨的類來擦除其他類的類型信息奸例。
比如說,我們需要寫一些能在特定幾個集合類型上面使用的代碼:
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
的泛型參數(shù)湖蜕。
總結(jié)
類型擦除是個非常有用的技術(shù)逻卖。他被用來阻止泛型對代碼的侵入, 也能夠讓接口更加的簡單。通過將底層的類型信息包裝起來, 將 API 和具體的功能分開昭抒。使用靜態(tài)的公有類型或者將 API 包裝進(jìn)函數(shù)都能夠做到類型擦除评也。基于函數(shù)做類型擦除對那種只需要幾個功能的簡單情況尤其有用灭返。
Swift 標(biāo)準(zhǔn)庫提供了一些可以直接使用的類型擦除盗迟。AnySequence
是 Sequence
的包裝, 從名字可以看出來, 他允許你在不知道具體類型的情況下迭代遍歷某個序列。AnyIterator
是他的好朋友, 它提供了一個類型已經(jīng)被擦除掉的迭代器婆殿。AnyHashable
包裝了類型擦除掉了的 Hashable
類型诈乒。Swift 中還有一些基于集合類型的協(xié)議。在文檔中搜索 “Any” 就可以看到婆芦。標(biāo)準(zhǔn)庫中的 Codable
也有用到了類型擦除: KeyedEncodingContainer
和 KeyedDecodingContainer
都是對應(yīng)協(xié)議類型擦除的包裝怕磨。他們用來在不知道具體類型信息的情況下實現(xiàn) encode 還有 decode喂饥。
最后
前幾天看到 MikeAsh 最新的 Friday Q&A Type Erasure in Swift。想趁著最近沒什么事情翻譯一下的肠鲫。結(jié)果最近一直沉迷吃雞员帮, 沒有時間去做這件事情。所以...
致讀者
前段時間的風(fēng)波過后, 很多小伙伴都以及離開了簡書這個平臺导饲。我自己也在 掘金 上開始了新的旅程捞高。但是在早前的學(xué)習(xí)過程中,查閱過大量在簡書上面的文章渣锦,甚至有段時間可以說是面向簡書編程硝岗。可以說在技術(shù)這條路上袋毙,簡書幫助了我很多⌒吞矗現(xiàn)在他不大歡迎程序員了。所以 之前的文章我不會刪除听盖,或者遷移到其他地方去胀溺,之后的文章全部都會在博客以外的掘金同步。但是不一定全部都會發(fā)在簡書上皆看。 每次都要在各個平臺都要去弄仓坞,真的很煩。