Swift 關聯類型

做者:Russ Bishop,原文連接,原文日期:2015-01-05
譯者:靛青K;校對:shanks;定稿:Ceehtml

我想要一個關聯類型的聖誕禮物ios

關聯類型系列文章

有時候我認爲類型理論是故意弄的很複雜,以及全部的那些函數式編程追隨者都只是胡說八道,彷彿他們理解了其中的含義。真的嗎?你有一篇 5000 字的博客是寫關於插入隨機類型理論概念的嗎?毫無疑問,沒有。並且這種理論沒法闡述你必需要關注它的緣由,以及經過這種高大上的概念能解決什麼樣的問題。我想把你裝進麻袋裏,而後把麻袋扔到河裏,最後把河丟到一個巨大的中。swift

咱們在討論什麼?固然,關聯類型。安全

當我第一次看到 Swift 泛型的實現時,關聯類型 的用法的出現,讓我感到很奇怪。app

在這篇文章,我將經過類型概念和一些實踐經驗,這幾乎都是我用本身的思考嘗試解釋這些概念(若是我犯了錯誤,請告訴我)。編程語言

泛型

在 Swift 中,若是我想有一個抽象的類型(也就是建立一個泛型的東西),在類中的語法是這個樣子:ide

class Wat<T> { ... }

相似的,帶泛型的結構體:

struct WatWat<T> { ... }

或者帶泛型的枚舉:

enum GoodDaySir<T> { ... }

但若是我想有一個抽象的協議:

protocol WellINever {
    typealias T
}

嗯哼?

基本概念

protocol 和 class、struct 以及 enum 不一樣,它不支持泛型類型參數。代替支持抽象類型成員;在 Swift 術語中稱做關聯類型。儘管你能夠用其它系統完成相似的事情,但這裏有一些使用關聯類型的好處(以及當前存在的一些缺點)。

協議中的一個關聯類型表示:「我不知道具體類型是什麼,一些服從個人類、結構體、枚舉會幫我實現這個細節」。

你會很驚奇:「很是棒,但和類型參數有什麼不一樣呢?」。這是一個很好的問題。類型參數強迫每一個人知道相關的類型以及須要反覆的指明該類型(當你在構建他們的時候,這會讓你寫不少的類型參數)。他們是公共接口的一部分。這些代碼使用多種結構(類、結構體、枚舉)的代碼會肯定具體選擇什麼類型。

經過對比關聯類型實現細節的部分。它被隱藏了,就像是一個類能夠隱藏內部的實例變量。使用抽象的類型成員的目的是推遲指明具體類型的時機。和泛型不一樣,它不是在實例化一個類或者結構體時指明具體類型,並且在服從該協議時,指明其具體類型。這讓咱們多了一種選擇類型的方式。

有用的

Scala 的建立者 Mark Odersky 在一次訪談中舉了一個例子。在 Swift 術語中,若是沒有關聯類型的話,此時你有一個帶有eat(f:Food) 的方法的基類或者協議 Animal ,以後的 Cow 類的沒有辦法指定 Food 只能是 Grass 。你很清楚不能經過重載這個方法 - 協變參數類型(在子類中添加一個更明確的參數)在大多數的語言都是不支持的,而且是一種不安全的方式 ,當從基類進行類型轉換的時候可能獲得意料以外的值。

譯者注:關於協變,您能夠參考這篇文章 Friday Q&A 2015-11-20:協變與逆變

若是 Swift 的協議已經支持類型參數,那代碼大概是這個樣子:

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

很是棒。那當咱們須要再增長些東西呢?

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) { ... }
}

增長了類型參數的數量是很不爽的,但這並非咱們的惟一問題。咱們處處泄露實現的細節,須要咱們去從新指明具體的類型。var c = Cow() 的類型就變成了 Cow<Grass,Salt> 。一個 doCowThings 方法將變成 func doCowThings(c:Cow<Grass,Salt>) 。那若是咱們想讓全部的動物都吃草呢?而且咱們沒有方式代表咱們不關心 Supplement 類型參數。

當咱們從 Cow 中得到了建立特別的品種,咱們的類就會很白癡的定義成這樣:class Holstein<Food:Grass, Supplement:Salt> : Cow<Grass,Salt>

更糟糕的是,一個買食物來餵養這些動物的方法變成這個樣子了:func buyFoodAndFeed<T,F where T:Animal<Food,Supplement>>(a:T, s:Store<F>) 。這真的很醜很囉嗦,咱們已經沒法把 FFood 關聯起來了。若是咱們重寫這個方法,咱們能夠這樣寫func buyFoodAndFeed<F:Food,S:Supplement>(a:Animal<Food,Supplement>, s:Store<Food>),但這並不會有做用 - 當咱們嘗試傳入一個 Cow<Grass, Salt> 參數,Swift 會抱怨 ’Grass’ is not identical to ‘Food’(’Grass’ 和 ‘Food’ 不相同)。再補充一點,注意到這個方法並不關心 Supplement ,但這裏咱們卻不得不處理它。

如今讓咱們看看如何用關聯類型幫咱們解決問題:

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){ ... }

如今的類型簽名清晰多了。Swift 指向這個關聯類型,只是經過查找 Cow 的方法簽名。咱們的 buyFoodAndFeed 方法,能夠清晰的表達商店賣的食物是動物吃的食物。事實上,Cow 須要一個特別的食物類型,而這個具體實現是在 Cow 類裏面,但這些信息仍然要在在編譯時肯定。

真實的例子

討論了一會關於動物的事情,讓咱們再來看看 Swift 中的 CollectionType

筆記: 做爲一個具體實現,許多 Swift 協議都有帶前導下劃線的嵌套協議;好比 CollectionType -> _CollectionType 或者 SequenceType -> _Sequence_Type -> _SequenceType。簡單來講,當咱們討論這些協議時,我即將打平這些層級。因此當我說 CollectionTypeItemTypeIndexTypeGeneratorType 關聯類型時,你並不能在協議 CollectionType 自己中找到這些。

顯然,咱們須要元素 T 的類型,但咱們也須要這個索引和生成器(generator)/計數器 (enumerator)的類型,這樣咱們才能夠處理 subscript(index:S) -> T { get }func generate() -> G<T> 。若是咱們只是使用類型參數,惟一的方法就是提供一個帶泛型的 Collection 協議,在一個假想的 CollectionOf<T,S,G> 中指明 T S G

其餘語言是怎麼處理的呢?C# 並無抽象類型成員。他首先處理這些是經過不支持任何東西而不是一個開放式的索引,這裏的類型系統不會代表索引是否只能單向移動,是否支持隨機存取等等。數字的索引就只是個整型,以及類型系統也只會代表這一信息。

其次,對於生成器 IEnumerable<T> 會生成一個 IEnumerator<T> 。起初這個不一樣看起來很是的微妙,但 C# 的解決方案是用一個接口(協議)直接的抽象覆蓋掉這個生成器,容許它避免必須去聲明特別的生成器類型,做爲一個參數,像 IEnumerable<T>

Swift 目的是作一個傳統的編譯系統(non-VM , non-JIT)編程語言,考慮到性能的需求,須要動態行爲類型並非一個好主意。編譯器真的傾向於知道你的索引和生成器的類型,以便於它能夠作一些奇妙的事情,好比代碼嵌入(inlining)以及知道須要分配多少內存這樣奇妙的事情。
惟一的方法就是,經過香腸研磨機在編譯時便利出全部的泛型。若是你強迫將它推遲到運行時,這也就意味着你須要一些間接的、裝箱和其餘的相似比較好的技巧,但這些都是有門檻的。

醜陋的事實

對於抽象類型成員,這兒有個大「坑」:Swift 不會徹底地讓你肯定他們是變量仍是參數類型,畢竟這是沒必要要的事情。只有在使用到泛型約束的時候,你纔會用到帶有關聯類型的協議。

在咱們的以前的 Animal 例子中,調用 Animal().eat 是不安全的,由於它只是一個抽象的 EdibleFood ,而且咱們不知道這個具體的類型。

理論上,這些代碼本應該能夠工做的,只要泛型在這個方法上強迫動物吃商店銷售的食物的約束,但實際上,當測試它的時候,我遇到了一些 EXC_BAD_ACCESS 的崩潰,我不肯定這是狀況是否是由於編譯器的問題。

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

咱們沒有辦法使用這些協議做爲參數或者變量類型。這只是須要考慮的更遠一些。這是一個我但願在將來 Swift 會支持的一個特性。我但願聲明變量或者類型時可以寫成這樣的代碼:

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

var x:GrassEatingAnimal = ...

注意:使用 typealias 只是建立一個類型別名,而不是在協議中的關聯類型。我知道這可能有些讓人感受困惑。

這個語法將會讓我能夠聲明持有關於一些動物中部分類型的一個變量,而這裏的動物關聯的 EdiableFoofGrass。這種語法在這種狀況下也頗有用:若是在協議中約束其關聯類型,但這看起來你可能會進入一個不安全的位置,致使須要考慮的更多一些。若是你開始運行時,有一件事,你須要約束關聯類型在這個編譯器的定義的協議不能安全的約束任何帶泛型的方法(見下文)。

當前狀況下,爲了得到一個類型參數,你必須經過建立一個封裝的結構體」擦除「其關聯類型。進一步的警告:這很醜陋。

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 標準庫會包括 GeneratorOf<T>:GeneratorSequenceOf<T>:SequenceSinkOf<T>:Sink...我想如今你知道了。

我上面提到的這個 bug ,若是 Animal 指明瞭 typealias EdibleFood:Food 以後,即便你給它定義了 typealias EdibleFood:Food ,這個結構體仍然是沒法編譯的。即便是在結構體中進行了清晰的約束, Swift 將會抱怨 F 不是 Food 。詳情能夠見 rdar://19371678 。

總結

就像咱們以前看到的,關聯類型容許在編譯時提供多個具體的類型,只要該類型服從對應的協議,從而不會用一堆類型參數污染類型定義。對於這個問題,它們是一個頗有趣的解決方案,用泛型類型參數表達出不一樣類型的抽象成員。

更進一步考慮,我在想,若是採起 Scala 的方案,簡單的爲 class、struct、enum 以及 protocol 提供類型參數和關聯類型兩個方法會是否更好一些。我尚未進行更深刻的思考,因此還有一些想法就先不討論了。對於一個新語言最讓人興奮的部分是——關注它的發展以及改進進度。

如今走的更遠一些,而且向你的同事開始炫耀相似抽象類型成員的東西。以後你也能夠稱霸他們,講一些很難理解的東西。

要遠離麻袋。

還有河水。

沒有坑,坑是使人驚奇的。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索