Swift如何使用類型擦除及自定義詳解
前言
在 Swift 的世界中,如果我們將協(xié)議稱之為國王,那么泛型則可以視作皇后,所謂一山不容二虎,當(dāng)我們把這兩者結(jié)合起來使用的時(shí)候,似乎會(huì)遇到極大的困難。那么是否有一種方法,能夠?qū)⑦@兩個(gè)概念結(jié)合在一起,以便讓它們成為我們前進(jìn)道路上的墊腳石,而不是礙手礙腳的呢?答案是有的,這里我們將會(huì)使用到類型擦除 (Type Erasure) 這個(gè)強(qiáng)大的特性。
你也許曾聽過類型擦除,甚至也使用過標(biāo)準(zhǔn)庫提供的類型擦除類型如 AnySequence。但到底什么是類型擦除? 如何自定義類型擦除? 在這篇文章中,我將討論如何使用類型擦除以及如何自定義。在此感謝 Lorenzo Boaro 提出這個(gè)主題。
有時(shí)你想對(duì)外部調(diào)用者隱藏某個(gè)類的具體類型,或是一些實(shí)現(xiàn)細(xì)節(jié)。在一些情況下,這樣做能防止靜態(tài)類型在項(xiàng)目中濫用,或者保證了類型間的交互。類型擦除就是移除某個(gè)類的具體類型使其變得更通用的過程。
協(xié)議或抽象父類可作為類型擦除簡單的實(shí)現(xiàn)方式之一。例如 NSString 就是一個(gè)例子,每次創(chuàng)建一個(gè) NSString 實(shí)例時(shí),這個(gè)對(duì)象并不是一個(gè)普通的 NSString 對(duì)象,它通常是某個(gè)具體的子類的實(shí)例,這個(gè)子類一般是私有的,同時(shí)這些細(xì)節(jié)通常是被隱藏起來的。你可以使用子類提供的功能而不用知道它具體的類型,你也沒必要將你的代碼與它們的具體類型聯(lián)系起來。
在處理 Swift 泛型以及關(guān)聯(lián)類型協(xié)議的時(shí)候,可能需要使用一些高級(jí)的內(nèi)容。Swift 不允許把協(xié)議當(dāng)做具體的類型來使用。例如,如果你想編寫一個(gè)方法,它的參數(shù)是一個(gè)包含了 Int 的序列,那么下面這種做法是不正確的:
func f(seq: Sequence<Int>) { ...
你不能這樣使用協(xié)議類型,這樣會(huì)在編譯時(shí)報(bào)錯(cuò)。但你可以使用泛型來替代協(xié)議,解決這個(gè)問題:
func f<S: Sequence>(seq: S) where S.Element == Int { ...
有時(shí)候這樣寫完全可以,但有些地方還存在一些比較麻煩的情況,通常你不可能只在一個(gè)地方添加泛型: 一個(gè)泛型函數(shù)對(duì)其他泛型要求更多… 更糟糕的是,你不能將泛型作為返回值或者屬性。這就跟我們想的有點(diǎn)不一樣了。
func g<S: Sequence>() -> S where S.Element == Int { ...
我們希望函數(shù) g 能返回任何符合的類型,但上面這個(gè)不同,它允許調(diào)用者選擇他所需要的類型,然后函數(shù) g 來提供一個(gè)合適的值。
Swift 標(biāo)準(zhǔn)庫中提供了 AnySequence 來幫助我們解決這個(gè)問題。AnySequence 包裝了一個(gè)任意類型的序列,并擦除了它的類型。使用 AnySequence 來訪問這個(gè)序列,我們來重寫一下函數(shù) f 與 函數(shù) g:
func f(seq: AnySequence<Int>) { ... func g() -> AnySequence<Int> { ...
泛型部分不見了,同時(shí)具體的類型也被隱藏起來了。由于使用了 AnySequence 包裝具體的值,它帶來了一定的代碼復(fù)雜性以及運(yùn)行時(shí)間成本。但是代碼卻更簡潔了。
Swift 標(biāo)準(zhǔn)庫中提供了很多這樣的類型,如 AnyCollection、AnyHashable 及 AnyIndex。這些類型在你自定義泛型或協(xié)議的時(shí)候非常的管用,你也可以直接使用這些類型來簡化你的代碼。接下來讓我們探索實(shí)現(xiàn)類型擦除的多種方式吧。
基于類的類型擦除
有時(shí)我們需要在不暴露類型信息的情況下從多個(gè)類型中包裝一些公共的功能,這聽起來就像是父類-子類的關(guān)系。事實(shí)上我們的確可以使用抽象父類來實(shí)現(xiàn)類型擦除。父類提供 API 接口,不用去管誰來實(shí)現(xiàn)。而子類根據(jù)具體的類型信息實(shí)現(xiàn)相應(yīng)的功能。
接下來我們將使用這種方式來自定義 AnySequence,我們將其命名為 MAnySequence:
class MAnySequence<Element>: Sequence {
這個(gè)類需要一個(gè) iterator 類型作為 makeIterator 返回類型。我們必須要做兩次類型擦除來隱藏底層的序列類型以及迭代器的類型。我們在 MAnySequence 內(nèi)部定義了一個(gè) Iterator 類,該類遵循著 IteratorProtocol 協(xié)議,并在 next() 方法中使用 fatalError 拋出異常。Swift 本身不支持抽象類型,但這樣也夠了:
class Iterator: IteratorProtocol { func next() -> Element? { fatalError("Must override next()") } }
MAnySequence 對(duì) makeIterator 方法實(shí)現(xiàn)也差不多。直接調(diào)用將拋出異常,這用來提示子類需要重寫這個(gè)方法:
func makeIterator() -> Iterator { fatalError("Must override makeIterator()") } }
這樣就定義了一個(gè)基于類的類型擦除的API,私有的子類將來實(shí)現(xiàn)這些API。公共類通過元素類型參數(shù)化,但私有實(shí)現(xiàn)類由它包裝的序列類型進(jìn)行參數(shù)化:
private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<Seq.Element> {
MAnySequenceImpl 需要一個(gè)繼承于 Iterator 的子類:
class IteratorImpl: Iterator {
IteratorImpl 包裝了序列的迭代器:
var wrapped: Seq.Iterator init(_ wrapped: Seq.Iterator) { self.wrapped = wrapped }
在 next 方法中調(diào)用被包裝的序列迭代器:
override func next() -> Seq.Element? { return wrapped.next() } }
相似地,MAnySequenceImpl 包裝一個(gè)序列:
var seq: Seq init(_ seq: Seq) { self.seq = seq }
從序列中獲取迭代器,然后將迭代器包裝成 IteratorImpl 對(duì)象返回,這樣就實(shí)現(xiàn)了 makeIterator 的功能。
override func makeIterator() -> IteratorImpl { return IteratorImpl(seq.makeIterator()) } }
我們需要一種方法來實(shí)際創(chuàng)建這些東西:對(duì) MAnySequence 添加一個(gè)靜態(tài)方法,該方法創(chuàng)建一個(gè) MAnySequenceImpl 實(shí)例,并將其作為 MAnySequence 類型返回給調(diào)用者。
extension MAnySequence { static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where Seq.Element == Element { return MAnySequenceImpl<Seq>(seq) } }
在實(shí)際開發(fā)中,我們可能會(huì)做一些額外的操作來讓 MAnySequence 提供一個(gè)初始化方法。
我們來試試 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í)我們希望對(duì)外暴露支持多種類型的方法,但又不想指定具體的類型。一個(gè)簡單的辦法就是,存儲(chǔ)那些簽名僅涉及到我們想公開的類型的函數(shù),函數(shù)主體在底層已知具體實(shí)現(xiàn)類型的上下文中創(chuàng)建。
我們一起看看如何運(yùn)用這種方法來設(shè)計(jì) MAnySequence,與前面的實(shí)現(xiàn)很類似。它是一個(gè)結(jié)構(gòu)體而非類,這是因?yàn)樗鼉H僅作為容器使用,不需要有任何的繼承關(guān)系。
struct MAnySequence<Element>: Sequence {
跟之前一樣,MAnySequence 也需要一個(gè)可返回的迭代器(Iterator)。迭代器同樣被設(shè)計(jì)為結(jié)構(gòu)體,并持有一個(gè)參數(shù)為空并返回 Element? 的存儲(chǔ)型屬性,實(shí)際上這個(gè)屬性是一個(gè)函數(shù),被用于 IteratorProtocol 協(xié)議的 next 方法中。接下來 Iterator 遵循 IteratorProtocol 協(xié)議,并在 next 方法中調(diào)用函數(shù):
struct Iterator: IteratorProtocol { let _next: () -> Element? func next() -> Element? { return _next() } }
MAnySequence 與 Iterator 很相似:持有一個(gè)參數(shù)為空返回 Iterator 類型的存儲(chǔ)型屬性。遵循 Sequence 協(xié)議并在 makeIterator 方法中調(diào)用這個(gè)屬性。
let _makeIterator: () -> Iterator func makeIterator() -> Iterator { return _makeIterator() }
MAnySequence 的構(gòu)造函數(shù)正是魔法起作用的地方,它接收任意序列作為參數(shù):
init<Seq: Sequence>(_ seq: Seq) where Seq.Element == Element {
接下來需要在構(gòu)造函數(shù)中包裝此序列的功能:
_makeIterator = {
如何生成迭代器?請(qǐng)求 Seq 序列生成:
var iterator = seq.makeIterator()
接下來我們利用自定義的迭代結(jié)構(gòu)體包裝序列生成的迭代器,包裝后的 _next 屬性將會(huì)在迭代器協(xié)議的 next() 方法中被調(diào)用:
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]))
正確運(yùn)行,太棒了!
當(dāng)需要將小部分功能包裝為更大類型的一部分時(shí),這種基于函數(shù)的類型擦除方法特別實(shí)用,這樣做就不需要有單獨(dú)的類來實(shí)現(xiàn)被擦除類型的這部分功能了。
比方說你現(xiàn)在想要編寫一些適用于各種集合類型的代碼,但它真正需要能夠?qū)@些集合執(zhí)行的操作是獲取計(jì)數(shù)并執(zhí)行從零開始的整數(shù)下標(biāo)。如訪問 tableView 數(shù)據(jù)源。它可能看起來像這樣:
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 其他代碼可通過調(diào)用 count() 或 getElement() 來操作傳入的集合。且不會(huì)讓集合類型破壞 GenericDataSource 泛型參數(shù)。
結(jié)束語
類型擦除是一種非常有用的技術(shù),它可用來阻止泛型對(duì)代碼的侵入,也可用來保證接口簡單明了。通過將底層類型包裝起來,將API與具體的功能進(jìn)行拆分。這可以通過使用抽象的公共超類和私有子類或?qū)?API 包裝在函數(shù)中來實(shí)現(xiàn)。對(duì)于只需要一些功能的簡單情況,基于函數(shù)類型擦除極其有效。
Swift 標(biāo)準(zhǔn)庫提供了幾種可直接利用的類型擦除類型。如 AnySequence 包裝一個(gè) Sequence,正如其名,AnySequence 允許你對(duì)序列迭代而無需知道序列具體的類型。AnyIterator 也是類型擦除的類型,它提供一個(gè)類型擦除的迭代器。AnyHashable 也同樣是類型擦除的類型,它提供了對(duì)Hashable類型訪問功能。Swift 還有很多基于集合的擦除類型,你可以通過搜索 Any 來查閱。標(biāo)準(zhǔn)庫中也為 Codable API 設(shè)計(jì)了類型擦除類型: KeyedEncodingContainer 和 KeyedDecodingContainer。它們都是容器協(xié)議類型包裝器,可用來在不知道底層具體類型信息的情況下實(shí)現(xiàn) Encode 和 Decode。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)我們的支持。
作者:Mike Ash,原文鏈接,原文日期:2017-12-18
譯者:rsenjoyer;校對(duì):Yousanflics,numbbbbb;定稿:Forelax
上一篇:Swift中優(yōu)雅處理閉包導(dǎo)致的循環(huán)引用詳解
欄 目:Swift
下一篇:Swift4.1轉(zhuǎn)場動(dòng)畫實(shí)現(xiàn)側(cè)滑抽屜效果
本文標(biāo)題:Swift如何使用類型擦除及自定義詳解
本文地址:http://mengdiqiu.com.cn/a1/Swift/11926.html
您可能感興趣的文章
- 01-11swift中defer幾個(gè)簡單的使用場景詳解
- 01-11Swift利用Decodable解析JSON的一個(gè)小問題詳解
- 01-11Swift中defer關(guān)鍵字推遲執(zhí)行示例詳解
- 01-11Swift中初始化init的方法小結(jié)
- 01-11Swift中定義單例的方法實(shí)例
- 01-11Swift利用純代碼實(shí)現(xiàn)時(shí)鐘效果實(shí)例代碼
- 01-11Swift中排序算法的簡單取舍詳解
- 01-11Swift如何為設(shè)置中心添加常用功能
- 01-11Swift Json實(shí)例詳細(xì)解析
- 01-11Swift利用指紋識(shí)別或面部識(shí)別為應(yīng)用添加私密保護(hù)功能


閱讀排行
本欄相關(guān)
- 01-11Swift利用Decodable解析JSON的一個(gè)小問題
- 01-11swift中defer幾個(gè)簡單的使用場景詳解
- 01-11Swift中初始化init的方法小結(jié)
- 01-11Swift中defer關(guān)鍵字推遲執(zhí)行示例詳解
- 01-11Swift利用純代碼實(shí)現(xiàn)時(shí)鐘效果實(shí)例代碼
- 01-11Swift中定義單例的方法實(shí)例
- 01-11Swift中排序算法的簡單取舍詳解
- 01-11Swift Json實(shí)例詳細(xì)解析
- 01-11Swift如何為設(shè)置中心添加常用功能
- 01-11Swift利用指紋識(shí)別或面部識(shí)別為應(yīng)用添
隨機(jī)閱讀
- 01-10C#中split用法實(shí)例總結(jié)
- 08-05織夢dedecms什么時(shí)候用欄目交叉功能?
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 04-02jquery與jsp,用jquery
- 01-10使用C語言求解撲克牌的順子及n個(gè)骰子
- 01-10delphi制作wav文件的方法
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 01-11ajax實(shí)現(xiàn)頁面的局部加載