[譯]Swift 中的類型擦除

找圖好辛苦

你可能聽過這個術(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 我們可以這樣來寫上面的 fg 方法斩萌。

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()")
        }
    }

ManySequencemakeIterator 方法的實現(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()
    }

MAnySequenceinit 方法是最重要的地方蕉世。他接受任意的 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)用到 iteratornext 函數(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)庫提供了一些可以直接使用的類型擦除盗迟。AnySequenceSequence 的包裝, 從名字可以看出來, 他允許你在不知道具體類型的情況下迭代遍歷某個序列。AnyIterator 是他的好朋友, 它提供了一個類型已經(jīng)被擦除掉的迭代器婆殿。AnyHashable 包裝了類型擦除掉了的 Hashable 類型诈乒。Swift 中還有一些基于集合類型的協(xié)議。在文檔中搜索 “Any” 就可以看到婆芦。標(biāo)準(zhǔn)庫中的 Codable 也有用到了類型擦除: KeyedEncodingContainerKeyedDecodingContainer 都是對應(yīng)協(xié)議類型擦除的包裝怕磨。他們用來在不知道具體類型信息的情況下實現(xiàn) encode 還有 decode喂饥。

最后

前幾天看到 MikeAsh 最新的 Friday Q&A Type Erasure in Swift。想趁著最近沒什么事情翻譯一下的肠鲫。結(jié)果最近一直沉迷吃雞员帮, 沒有時間去做這件事情。所以...

致讀者

前段時間的風(fēng)波過后, 很多小伙伴都以及離開了簡書這個平臺导饲。我自己也在 掘金 上開始了新的旅程捞高。但是在早前的學(xué)習(xí)過程中,查閱過大量在簡書上面的文章渣锦,甚至有段時間可以說是面向簡書編程硝岗。可以說在技術(shù)這條路上袋毙,簡書幫助了我很多⌒吞矗現(xiàn)在他不大歡迎程序員了。所以 之前的文章我不會刪除听盖,或者遷移到其他地方去胀溺,之后的文章全部都會在博客以外的掘金同步。但是不一定全部都會發(fā)在簡書上皆看。 每次都要在各個平臺都要去弄仓坞,真的很煩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腰吟,一起剝皮案震驚了整個濱河市无埃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蝎困,老刑警劉巖录语,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異禾乘,居然都是意外死亡,警方通過查閱死者的電腦和手機虽缕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門始藕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人氮趋,你說我怎么就攤上這事伍派。” “怎么了剩胁?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵诉植,是天一觀的道長。 經(jīng)常有香客問我昵观,道長晾腔,這世上最難降的妖魔是什么舌稀? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮灼擂,結(jié)果婚禮上壁查,老公的妹妹穿的比我還像新娘。我一直安慰自己剔应,他們只是感情好睡腿,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著峻贮,像睡著了一般席怪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纤控,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天何恶,我揣著相機與錄音,去河邊找鬼嚼黔。 笑死细层,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的唬涧。 我是一名探鬼主播疫赎,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼碎节!你這毒婦竟也來了捧搞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤狮荔,失蹤者是張志新(化名)和其女友劉穎胎撇,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體殖氏,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡晚树,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了雅采。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爵憎。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖婚瓜,靈堂內(nèi)的尸體忽然破棺而出宝鼓,到底是詐尸還是另有隱情,我是刑警寧澤巴刻,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布愚铡,位于F島的核電站,受9級特大地震影響胡陪,放射性物質(zhì)發(fā)生泄漏沥寥。R本人自食惡果不足惜碍舍,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望营曼。 院中可真熱鬧乒验,春花似錦、人聲如沸蒂阱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽录煤。三九已至鳄厌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妈踊,已是汗流浹背了嚎。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留廊营,地道東北人歪泳。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像露筒,于是被迫代替她去往敵國和親呐伞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345