老司机带你深入浅出 Collection
作者:Ole Begemann,原文鏈接,原文日期:2016-09-22
譯者:BigbigChai;校對:walkingway;定稿:CMB
本文摘自即將出新版的 Swift 進階(Advanced Swift)一書中的集合協議(Collection Protocols)章節(稍作修改以適合博客文章)。我和 Chris Eidhof 已經基本完成為本書更新到 Swift 3 的工作,很快可以面世。
Swift 中的集合非常強大,但也很復雜。如果你想實現自定義的集合類型,首先需要了解集合協議的原理。即使只是使用標準庫中常見的集合類型,它的工作原理仍然十分值得學習,尤其是它可以幫助你理解編譯器打印出來的錯誤信息。
在本文中,我們想探討一下集合協議的關聯類型。這聽起來像是一個晦澀的主題,但我認為想要掌握 Swift 中集合類型的關鍵:在于對理解關聯類型的作用、以及為什么需要它們。
概述
集合有五種關聯類型。它們聲明如下(實際的代碼并不是這樣,因為 Index 是在 IndexableBase 中聲明的,但你明白我的意思就好):
protocol Collection: Indexable, Sequence {associatedtype Iterator: IteratorProtocol = IndexingIterator<Self>associatedtype SubSequence: IndexableBase, Sequence = Slice<Self>associatedtype Index: Comparable // declared in IndexableBaseassociatedtype IndexDistance: SignedInteger = Intassociatedtype Indices: IndexableBase, Sequence = DefaultIndices<Self>... }前四個關聯類型繼承自基礎協議
Sequence,Indexable 和 IndexableBase 1;集合遵循了以上所有的協議,只是 Index 的約束更加嚴格、余下的協議賦予了不同的默認值。
注意,除了 Index 以外,集合類型的關聯類型都有默認值 — 因此遵守集合協議的類型都只需指定 Index 的類型就可以了。雖然你不必過分在意其他的關聯類型,但還是應該大致了解一下。
迭代器 Iterator
遵守 Sequence 協議。Sequence 通過創建迭代器來訪問它們的元素。迭代器每次產生一個序列的值,并在遍歷該序列時追蹤它的迭代狀態。
迭代器內部有一個稱為 Element 的關聯類型。Element 類型指定了迭代器的生成值類型。例如,對于 String.CharacterView 的迭代器而言,Element 的類型是 Character。另外,迭代器也定義了它的 Sequence 的 Element 類型;事實上,我們經常能在方法簽名、或者 Sequence 和集合的泛型約束中看到對 Iterator.Element 的引用,就是因為 Element 是 IteratorProtocol 的關聯類型。
集合的默認迭代器類型是 IndexingIterator <Self>。這是一個非常簡單的封裝結構體,它使用集合自身的索引來遍歷每個元素。標準庫中的大多數集合都使用 IndexingIterator 作為迭代器。我們不需要為自定義的集合更改迭代器類型。
子序列 SubSequence
子序列也遵守 Sequence 協議,但是集合約束更加嚴格:集合的子序列本身也應該是集合。(我們說“應該”而不是“必須”,因為這種約束在目前的類型系統中無法完全表示。)
在返回初始集合片段的操作中,子序列作為其返回類型:
prefix 和 suffix — 取開頭或末尾的 n 個元素。
dropFirst 和 dropLast — 返回刪除開頭或末尾 n 個元素后的子序列。
拆分(split) — 以指定的分隔符元素拆分序列,并以數組形式返回。
帶有 Range <Index> 參數的 subscript(下標)— 返回指定索引范圍內的元素片段。
集合的默認子序列類型是 Slice <Self>,它封裝了初始的集合(類似于 IndexingIterator ),并存儲該片段在初始集合中的起始索引(startIndex)和結束索引(endIndex)。
自定義集合的子序列類型非常有用,特別是當它定義為 Self(即集合的片段與集合本身類型相同)的時候。標準庫類型中的例子有 String.CharacterView,這讓字符串片段的使用更為方便。而一個反例是 Array,它以 ArraySlice 作為片段類型。
索引 Index
索引表示集合中的位置。每個集合都有兩個特殊的索引,startIndex 和 endIndex。 startIndex 指向集合的第一個元素,而 endIndex 是集合中最后一個元素之后的索引。索引應該是一個啞值,只存儲表明元素位置所需的最少信息量。尤其,索引應該盡可能地減少對集合的引用。集合索引必須是可比較的,這是它唯一的要求。也就是說,索引需要有明確的順序。
比如數組就是用整數作為索引的,但是整數索引不是對所有數據結構都起作用。我們再以 String.CharacterView 為例,Swift 中的字符是大小可變的;如果你想使用整數索引,你有兩個選擇:
用索引表示字符串內部存儲的偏移量。這種做法十分有效率;訪問一個給定索引的元素的復雜度是 O(1)。但是對于索引范圍而言會有差別。例如,如果索引 0 處的字符是正常大小的兩倍,則下一個字符的索引會是 2 - 訪問索引 1 處的元素將觸發致命錯誤或未定義行為。這會嚴重違反用戶的期望。
用索引 n 表示字符串中的第 n 個字符。這與用戶期望一致 — 對索引范圍來說不會有任何差別。然而,訪問給定索引的元素的復雜度變成了O(n);必須從頭遍歷字符串內該索引之前的所有元素,才能確定字符的儲存位置。這種行為非常不好,因為用戶會期望通過索引下標訪問元素的操作能瞬間完成。
?
因此,String.CharacterView.Index 是一個不可見的值,指向字符串的內部存儲緩沖區中的位置。實際上,它只是封裝了一個整型偏移量,集合的使用者并不會對這種實現細節感興趣。
每個集合都需要分別選擇正確的索引類型。因此,關聯類型中索引是唯一沒有默認值的。
索引距離 IndexDistance
索引距離是一個帶符號的整型,表示兩個索引之間的距離。默認值是整型,我們沒必要自己修改。
索引范圍 Indices
這是集合的 indices 屬性的返回類型。它是一個包含所有索引的集合,該集合中的索引以升序排列對應初始集合的下標。注意,endIndex 不包括在內,因為 endIndex 表示”結束之后”的位置,所以不是有效的下標參數。
在 Swift 2 中,indices 屬性返回一個 Range <Index>,可以用來遍歷集合中所有的有效索引。在 Swift 3 中,Range <Index> 不再可迭代,因為索引不能自我遞進(現在由集合來推進索引迭代)。Indices 類型替代了 Range <Index> 來實現索引的迭代。
默認的 Indices 類型十分具有想象力地命名為 DefaultIndices <Self>(23333)。它跟 Slice 一樣,是對初始集合、起始和結束索引的一個簡單封裝 — 它需要保留對初始集合的引用,以便能夠推進索引。如果在集合迭代索引的過程中對集合進行修改,可能會導致意想不到的性能問題:假設集合的實現使用了寫時復制(copy-on-write)(正如標準庫中的所有集合類型),迭代開始之后對集合的額外引用可能觸發不必要的復制。
我們在書中廣泛說明了寫時復制的內容。就現在來說,知道自定義集合可以使用一個不引用初始集合的 Indices 類型就足夠了,這樣做是一個非常有益的優化。所有索引不依賴于集合本身的集合都可以這樣使用,例如數組。如果數組的索引是一個整數類型,你可以使用 CountableRange <Index>。以下是對自定義隊列類型的定義(我們在書中實現了此類型):
extension Queue: Collection {...typealias Indices = CountableRange<Int>var indices: CountableRange<Int> {return startIndex..<endIndex} }本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg。
考慮這是更好地支持泛型的部分,希望明年當這個讓人高度期待的特性出來時,Indexable 和 IndexableBase 會被去掉,而把它們的功能放在集合內部。 ?
總結
以上是生活随笔為你收集整理的老司机带你深入浅出 Collection的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android Studio 使用Lam
- 下一篇: 读取pandas修改单列数据类型