Swift 關聯類型

MartinaLink 8年前發布 | 6K 次閱讀 Swift Apple Swift開發

有時候我認為類型理論是故意弄的很復雜,以及所有的那些函數式編程追隨者都只是胡說八道,仿佛他們理解了其中的含義。真的嗎?你有一篇 5000 字的博客是寫關于 插入隨機類型理論概念 的嗎?毫無疑問的沒有。a)為什么有人會關心這些以及b)通過這個高大上的概念能幫我們解決什么問題?我想把你裝進麻布袋里,扔進河里,并且砸進一個 里。

我們在討論什么?當然,關聯類型。

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

在這篇文章,我將通過類型概念和一些實踐經驗,這幾乎都是我用自己的思考嘗試解釋這些概念(如果我犯了錯誤,請告訴我)。

范型

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

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 。你很清楚不能通過重載這個方法 - 協變參數類型(在子類中添加一個更明確的參數)在大多數的語言都是不支持的,并且是一種不安全的方式 ,當從基類進行類型轉換的時候可能得到意料之外的值。

如果 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>) 。這真的很丑很啰嗦,我們已經無法把 F 和 Food 關聯起來了。如果我們重寫這個方法,我們可以這樣寫 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 。簡單來說,當我們討論這些協議時,我即將打平這些層級。所以當我說 CollectionType 有 ItemType 、 IndexType 和 GeneratorType 關聯類型時,你并不能在協議 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)以及知道需要分配多少內存這樣奇妙的事情。

唯一的方法就是,通過 香腸研磨機 在編譯時便利出所有的泛型。如果你強迫將它推遲到運行時,這也就意味著你需要一些間接的、裝箱和其他的類似比較好的技巧,但這些都是有門檻的。

愚蠢的事實

這里主要的帶有抽象類型成員的 “gotcha” :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 只是創建一個類型別名,而不是在協議中的關聯類型。我知道這可能有些讓人感覺困惑。

這個語法將會讓我聲明一個變量可以持有一些動物的一些類型,而這里的動物關聯的 EdiableFoof 是 Grass 。它可能是很有用的,如果在協議中約束其關聯類型,但這看起來你可能會進入一個不安全的位置,導致需要考慮的更多一些。如果你開始運行時,有一件事,你需要 約束 關聯類型在這個編譯器的定義的協議不能安全的約束任何帶泛型的方法(見下文)。

當前情況下,為了獲得一個類型參數,你必須通過創建一個封裝的結構體”擦除“其關聯類型。進一步的警告:這很丑陋。

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>:Generator 、 SequenceOf<T>:Sequence 和 SinkOf<T>:Sink … 我想現在你知道了。

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

總結

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

更進一步考慮,我在想,如果采取 Scala 的方案,簡單的為 class 、 struct 、enum 以及 protocol 提供類型參數和關聯類型兩個方法會是否更好一些。我還沒有進行更深入的思考,所以還有一些想法就先不討論了。對于一個新語言最讓人興奮的部分是 - 關注它的發展以及改進進度。

現在走的更遠一些,并且向你的同事開始炫耀類似 抽象類型成員 的東西。之后你也可以稱霸他們,講一些很難理解的東西。

 

來自:https://segmentfault.com/a/1190000007124264

 

 本文由用戶 MartinaLink 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!