作者:Russ Bishop笆凌,原文鏈接赊窥,原文日期:2015-01-05
譯者:靛青K靶橱;校對(duì):shanks佳遂;定稿:CMB
我想要一個(gè)關(guān)聯(lián)類型的圣誕禮物
關(guān)聯(lián)類型系列文章
- Swift 關(guān)聯(lián)類型
- Swift 關(guān)聯(lián)類型营袜,續(xù)
- Swift :為什么選擇關(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)無法把 F
和 Food
關(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)我說CollectionType
有ItemType
、IndexType
和GeneratorType
關(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)的 EdiableFoof
是 Grass
。它可能是很有用的峻仇,如果在協(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>:Generator
、SequenceOf<T>:Sequence
和 SinkOf<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耐薯。