Swift 關(guān)聯(lián)類型

作者:Russ Bishop笆凌,原文鏈接赊窥,原文日期:2015-01-05
譯者:靛青K靶橱;校對(duì):shanks佳遂;定稿:CMB

我想要一個(gè)關(guān)聯(lián)類型的圣誕禮物

關(guān)聯(lián)類型系列文章

有時(shí)候我認(rèn)為類型理論是故意弄的很復(fù)雜,以及所有的那些函數(shù)式編程追隨者都只是胡說八道丑罪,仿佛他們理解了其中的含義荚板。真的嗎?你有一篇 5000 字的博客是寫關(guān)于插入隨機(jī)類型理論概念的嗎吩屹?毫無疑問的沒有啸驯。a)為什么有人會(huì)關(guān)心這些以及b)通過這個(gè)高大上的概念能幫我們解決什么問題?我想把你裝進(jìn)麻布袋里祟峦,扔進(jìn)河里,并且砸進(jìn)一個(gè)里徙鱼。

我們在討論什么宅楞?當(dāng)然,關(guān)聯(lián)類型袱吆。

當(dāng)我第一次看到 Swift 范型的實(shí)現(xiàn)時(shí)厌衙,關(guān)聯(lián)類型 的用法的出現(xiàn),讓我感到很奇怪绞绒。

在這篇文章婶希,我將通過類型概念和一些實(shí)踐經(jīng)驗(yàn),這幾乎都是我用自己的思考嘗試解釋這些概念(如果我犯了錯(cuò)誤蓬衡,請告訴我)喻杈。

范型

在 Swift 中,如果我想有一個(gè)抽象的類型(也就是創(chuàng)建一個(gè)范型的東西)狰晚,在類中的語法是這個(gè)樣子:

class Wat<T> { ... }

類似的筒饰,帶范型的結(jié)構(gòu)體:

struct WatWat<T> { ... }

或者帶范型的枚舉:

enum GoodDaySir<T> { ... }

但如果我想有一個(gè)抽象的協(xié)議:

protocol WellINever {
    typealias T
}

嗯哼?

基本概念

protocol 和 class 壁晒、struct 以及 enum 不同瓷们,它不支持范型類型參數(shù)。代替支持抽象類型成員秒咐;在 Swift 術(shù)語中稱作關(guān)聯(lián)類型谬晕。盡管你可以用其它系統(tǒng)完成類似的事情,但這里有一些使用關(guān)聯(lián)類型的好處(以及當(dāng)前存在的一些缺點(diǎn))携取。

協(xié)議中的一個(gè)關(guān)聯(lián)類型表示:“我不知道具體類型是什么攒钳,一些服從我的類、結(jié)構(gòu)體歹茶、枚舉會(huì)幫我實(shí)現(xiàn)這個(gè)細(xì)節(jié)”夕玩。

你會(huì)很驚奇:“非常棒你弦,但和類型參數(shù)有什么不同呢?”燎孟。一個(gè)很好的問題禽作。類型參數(shù)強(qiáng)迫每個(gè)人知道相關(guān)的類型以及需要反復(fù)的指明該類型(當(dāng)你在構(gòu)建他們的時(shí)候,這會(huì)讓你寫很多的類型參數(shù))揩页。他們是公共接口的一部分旷偿。這些代碼使用多種結(jié)構(gòu)(類、結(jié)構(gòu)體爆侣、枚舉)的代碼會(huì)確定具體選擇什么類型萍程。

通過對(duì)比關(guān)聯(lián)類型實(shí)現(xiàn)細(xì)節(jié)的部分。它被隱藏了兔仰,就像是一個(gè)類可以隱藏內(nèi)部的實(shí)例變量茫负。使用抽象的類型成員的目的是推遲指明具體類型的時(shí)機(jī)。和泛型不同乎赴,它不是在實(shí)例化一個(gè)類或者結(jié)構(gòu)體時(shí)指明具體類型忍法,而且在服從該協(xié)議時(shí),指明其具體類型榕吼。這讓我們多了一種選擇類型的方式饿序。

有用的

Scala 的創(chuàng)建者 Mark Odersky 在一次交流時(shí)討論了一個(gè)例子。在 Swift 術(shù)語中羹蚣,如果沒有關(guān)聯(lián)類型的話原探,此時(shí)你有一個(gè)帶有eat(f:Food) 的方法的基類或者協(xié)議 Animal ,之后的Cow 類的沒有辦法指定 Food 只能是 Grass 顽素。你很清楚不能通過重載這個(gè)方法 - 協(xié)變參數(shù)類型(在子類中添加一個(gè)更明確的參數(shù))在大多數(shù)的語言都是不支持的咽弦,并且是一種不安全的方式 ,當(dāng)從基類進(jìn)行類型轉(zhuǎn)換的時(shí)候可能得到意料之外的值戈抄。

譯者注:關(guān)于協(xié)變离唬,您可以參考這篇文章Friday Q&A 2015-11-20:協(xié)變與逆變

如果 Swift 的協(xié)議已經(jīng)支持類型參數(shù)划鸽,那代碼大概是這個(gè)樣子:

protocol Food { }
class Grass : Food { }
protocol Animal<F:Food> {
    func eat(f:F)
}
class Cow : Animal<Grass> {
    func eat(f:Grass) { ... }
}

非常棒输莺。那當(dāng)我們需要再增加些東西呢?

protocol Animal<F:Food, S:Supplement> {
    func eat(f:F)
    func supplement(s:S)
}
class Cow : Animal<Grass, Salt> {
    func eat(f:Grass) { ... }
    func supplement(s:Salt) { ... }
}

增加了類型參數(shù)的數(shù)量是很不爽的裸诽,但這并不是我們的唯一問題嫂用。我們到處泄露實(shí)現(xiàn)的細(xì)節(jié),需要我們?nèi)ブ匦轮该骶唧w的類型丈冬。var c = Cow() 的類型就變成了 Cow<Grass,Salt> 嘱函。一個(gè) doCowThings 方法將變成 func doCowThings(c:Cow<Grass,Salt>) 。那如果我們想讓所有的動(dòng)物都吃草呢埂蕊?并且我們沒有方式表明我們不關(guān)心 Supplement 類型參數(shù)往弓。

當(dāng)我們從 Cow 中獲得了創(chuàng)建特別的品種疏唾,我們的類就會(huì)很白癡的定義成這樣:class Holstein<Food:Grass, Supplement:Salt> : Cow<Grass,Salt>

更糟糕的是函似,一個(gè)買食物來喂養(yǎng)這些動(dòng)物的方法變成這個(gè)樣子了:func buyFoodAndFeed<T,F where T:Animal<Food,Supplement>>(a:T, s:Store<F>) 槐脏。這真的很丑很啰嗦,我們已經(jīng)無法把 FFood 關(guān)聯(lián)起來了撇寞。如果我們重寫這個(gè)方法顿天,我們可以這樣寫func buyFoodAndFeed<F:Food,S:Supplement>(a:Animal<Food,Supplement>, s:Store<Food>),但這并不會(huì)有作用 - 當(dāng)我們嘗試傳入一個(gè) Cow<Grass, Salt> 參數(shù)蔑担,Swift 會(huì)抱怨 ’Grass’ is not identical to ‘Food’(’Grass’ 和 ‘Food’ 不相同)牌废。再補(bǔ)充一點(diǎn),注意到這個(gè)方法并不關(guān)心 Supplement 啤握,但這里我們卻不得不處理它鸟缕。

現(xiàn)在讓我們看看如何用關(guān)聯(lián)類型幫我們解決問題:

protocol Animal {
    typealias EdibleFood
    typealias SupplementKind
    func eat(f:EdibleFood)
    func supplement(s:SupplementKind)
}
class Cow : Animal {
    func eat(f: Grass) { ... }
    func supplement(s: Salt) { ... }
}
class Holstein : Cow { ... }
func buyFoodAndFeed<T:Animal, S:Store where T.EdibleFood == S.FoodType>(a:T, s:S){ ... }

現(xiàn)在的類型簽名清晰多了。Swift 指向這個(gè)關(guān)聯(lián)類型排抬,只是通過查找 Cow 的方法簽名叁扫。我們的 buyFoodAndFeed 方法捌省,可以清晰的表達(dá)商店賣的食物是動(dòng)物吃的食物的诵。事實(shí)上翻伺,Cow 需要一個(gè)特別的食物類型,而這個(gè)具體實(shí)現(xiàn)是在 Cow 類里面悠鞍,但這些信息仍然要在在編譯時(shí)確定。

真實(shí)的例子

討論了一會(huì)關(guān)于動(dòng)物的事情模燥,讓我們再來看看 Swift 中的 CollectionType 咖祭。

筆記: 作為一個(gè)具體實(shí)現(xiàn),許多 Swift 協(xié)議都有帶前導(dǎo)下劃線的嵌套協(xié)議蔫骂;比如 CollectionType -> _CollectionType 或者 SequenceType -> _Sequence_Type -> _SequenceType么翰。簡單來說,當(dāng)我們討論這些協(xié)議時(shí)辽旋,我即將打平這些層級(jí)浩嫌。所以當(dāng)我說 CollectionTypeItemTypeIndexTypeGeneratorType 關(guān)聯(lián)類型時(shí)补胚,你并不能在協(xié)議 CollectionType 本身中找到這些码耐。

顯然,我們需要元素 T 的類型溶其,但我們也需要這個(gè)索引和生成器(generator)/計(jì)數(shù)器 (enumerator)的類型骚腥,這樣我們才可以處理 subscript(index:S) -> T { get }func generate() -> G<T> 。如果我們只是使用類型參數(shù)瓶逃,唯一的方法就是提供一個(gè)帶泛型的 Collection 協(xié)議束铭,在一個(gè)假想的 CollectionOf<T,S,G> 中指明 T S G 廓块。

其他語言是怎么處理的呢?C# 并沒有抽象類型成員契沫。他首先處理這些是通過不支持任何東西而不是一個(gè)開放式的索引带猴,這里的類型系統(tǒng)不會(huì)表明索引是否只能單向移動(dòng),是否支持隨機(jī)存取等等埠褪。數(shù)字的索引就只是個(gè)整型浓利,以及類型系統(tǒng)也只會(huì)表明這一信息。

其次钞速,對(duì)于生成器 IEnumerable<T> 會(huì)生成一個(gè) IEnumerator<T> 贷掖。起初這個(gè)不同看起來非常的微妙,但 C# 的解決方案是用一個(gè)接口(協(xié)議)直接的抽象覆蓋掉這個(gè)生成器渴语,允許它避免必須去聲明特別的生成器類型苹威,作為一個(gè)參數(shù),像 IEnumerable<T> 驾凶。

Swift 目的是做一個(gè)傳統(tǒng)的編譯系統(tǒng)(non-VM 牙甫, non-JIT)編程語言,考慮到性能的需求调违,需要?jiǎng)討B(tài)行為類型并不是一個(gè)好主意窟哺。編譯器真的傾向于知道你的索引和生成器的類型,以便于它可以做一些奇妙的事情技肩,比如代碼嵌入(inlining)以及知道需要分配多少內(nèi)存這樣奇妙的事情且轨。
唯一的方法就是,通過香腸研磨機(jī)在編譯時(shí)便利出所有的泛型虚婿。如果你強(qiáng)迫將它推遲到運(yùn)行時(shí)旋奢,這也就意味著你需要一些間接的、裝箱和其他的類似比較好的技巧然痊,但這些都是有門檻的至朗。

愚蠢的事實(shí)

這里主要的帶有抽象類型成員的 “gotcha” :Swift 不會(huì)完全地讓你確定他們是變量還是參數(shù)類型,畢竟這是不必要的事情剧浸。只有在使用到泛型約束的時(shí)候锹引,你才會(huì)用到帶有關(guān)聯(lián)類型的協(xié)議。

在我們的之前的 Animal 例子中唆香,調(diào)用 Animal().eat 是不安全的粤蝎,因?yàn)樗皇且粋€(gè)抽象的 EdibleFood ,并且我們不知道這個(gè)具體的類型袋马。

理論上初澎,這些代碼本應(yīng)該可以工作的,只要泛型在這個(gè)方法上強(qiáng)迫動(dòng)物吃商店銷售的食物的約束,但實(shí)際上碑宴,當(dāng)測試它的時(shí)候软啼,我遇到了一些 EXC_BAD_ACCESS 的崩潰,我不確定這是情況是不是因?yàn)榫幾g器的問題延柠。

func buyFoodAndFeed<T:Animal,S:StoreType where T.EdibleFood == S.FoodType>(a:T, s:S) {
    a.eat(s.buyFood()) //crash!
}

我們沒有辦法使用這些協(xié)議作為參數(shù)或者變量類型祸挪。這只是需要考慮的更遠(yuǎn)一些。這是一個(gè)我希望在未來 Swift 會(huì)支持的一個(gè)特性贞间。我希望聲明變量或者類型時(shí)能夠?qū)懗蛇@樣的代碼:

typealias GrassEatingAnimal = protocol<A:Animal where A.EdibleFood == Grass>

var x:GrassEatingAnimal = ...

注意:使用 typealias 只是創(chuàng)建一個(gè)類型別名贿条,而不是在協(xié)議中的關(guān)聯(lián)類型。我知道這可能有些讓人感覺困惑增热。

這個(gè)語法將會(huì)讓我聲明一個(gè)變量可以持有一些動(dòng)物的一些類型整以,而這里的動(dòng)物關(guān)聯(lián)的 EdiableFoofGrass 。它可能是很有用的峻仇,如果在協(xié)議中約束其關(guān)聯(lián)類型公黑,但這看起來你可能會(huì)進(jìn)入一個(gè)不安全的位置,導(dǎo)致需要考慮的更多一些摄咆。如果你開始運(yùn)行時(shí)凡蚜,有一件事,你需要約束關(guān)聯(lián)類型在這個(gè)編譯器的定義的協(xié)議不能安全的約束任何帶泛型的方法(見下文)吭从。

當(dāng)前情況下朝蜘,為了獲得一個(gè)類型參數(shù),你必須通過創(chuàng)建一個(gè)封裝的結(jié)構(gòu)體”擦除“其關(guān)聯(lián)類型涩金。進(jìn)一步的警告:這很丑陋芹务。

struct SpecificAnimal<F,S> : Animal {
    let _eat:(f:F)->()
    let _supplement:(s:S)->()
    init<A:Animal where A.EdibleFood == F, A.SupplementKind == S>(var _ selfie:A) {
        _eat = { selfie.eat($0) }
        _supplement = { selfie.supplement($0) }
    }
    func eat(f:F) {
        _eat(f:f)
    }
    func supplement(s:S) {
        _supplement(s:s)
    }
}

如果你曾考慮過為什么 Swift 標(biāo)準(zhǔn)庫會(huì)包括 GeneratorOf<T>:GeneratorSequenceOf<T>:SequenceSinkOf<T>:Sink … 我想現(xiàn)在你知道了鸭廷。

我上面提到的這個(gè) bug ,如果 Animal 指明了 typealias EdibleFood:Food 之后熔吗,即使你給它定義了 typealias EdibleFood:Food 辆床,這個(gè)結(jié)構(gòu)體仍然是無法編譯的。即使是在結(jié)構(gòu)體中進(jìn)行了清晰的約束桅狠, Swift 將會(huì)抱怨 F 不是 Food 讼载。詳情可以見 rdar://19371678 。

總結(jié)

就像我們之前看到的中跌,關(guān)聯(lián)類型允許在編譯時(shí)提供多個(gè)具體的類型咨堤,只要該類型服從對(duì)應(yīng)的協(xié)議,從而不會(huì)用一堆類型參數(shù)污染類型定義漩符。對(duì)于這個(gè)問題一喘,它們是一個(gè)很有趣的解決方案,用泛型類型參數(shù)表達(dá)出不同類型的抽象成員。

更進(jìn)一步考慮凸克,我在想议蟆,如果采取 Scala 的方案,簡單的為 class 萎战、 struct 咐容、enum 以及 protocol 提供類型參數(shù)和關(guān)聯(lián)類型兩個(gè)方法會(huì)是否更好一些。我還沒有進(jìn)行更深入的思考蚂维,所以還有一些想法就先不討論了戳粒。對(duì)于一個(gè)新語言最讓人興奮的部分是 - 關(guān)注它的發(fā)展以及改進(jìn)進(jìn)度。

現(xiàn)在走的更遠(yuǎn)一些虫啥,并且向你的同事開始炫耀類似抽象類型成員的東西蔚约。之后你也可以稱霸他們,講一些很難理解的東西孝鹊。

要遠(yuǎn)離麻袋炊琉。

還有河水。

沒有坑又活,坑是令人驚奇的苔咪。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán)柳骄,最新文章請?jiān)L問 http://swift.gg团赏。,最新文章請?jiān)L問 http://swift.gg耐薯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舔清,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子曲初,更是在濱河造成了極大的恐慌体谒,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臼婆,死亡現(xiàn)場離奇詭異抒痒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)颁褂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門故响,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颁独,你說我怎么就攤上這事彩届。” “怎么了誓酒?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵樟蠕,是天一觀的道長。 經(jīng)常有香客問我,道長坯墨,這世上最難降的妖魔是什么寂汇? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮捣染,結(jié)果婚禮上骄瓣,老公的妹妹穿的比我還像新娘。我一直安慰自己耍攘,他們只是感情好榕栏,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蕾各,像睡著了一般扒磁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上式曲,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天妨托,我揣著相機(jī)與錄音,去河邊找鬼吝羞。 笑死兰伤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钧排。 我是一名探鬼主播敦腔,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼恨溜!你這毒婦竟也來了符衔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤糟袁,失蹤者是張志新(化名)和其女友劉穎判族,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體项戴,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡形帮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肯尺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡躯枢,死狀恐怖则吟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锄蹂,我是刑警寧澤氓仲,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響敬扛,放射性物質(zhì)發(fā)生泄漏晰洒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一啥箭、第九天 我趴在偏房一處隱蔽的房頂上張望谍珊。 院中可真熱鬧,春花似錦急侥、人聲如沸砌滞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贝润。三九已至,卻和暖如春铝宵,著一層夾襖步出監(jiān)牢的瞬間打掘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來泰國打工鹏秋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尊蚁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓拼岳,卻偏偏與公主長得像枝誊,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子惜纸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容