Swift進階三: 集合

集合

這里的集合指的是建立在Sequence協(xié)議上的Collection協(xié)議。
集合是穩(wěn)定的序列,可以重復遍歷而不會發(fā)生變化,除了遍歷還可以使用下標訪問,swift的下標不一定是整數(shù),但一定是一個有限的范圍,因此集合不能是無限的,如此,集合也產(chǎn)生了長度的屬性count。

通過定義一個集合類型來理解Collection如何工作。
首先設計一個隊列協(xié)議:

protocol Queue{
    //聲明類型
    associatedtype Element
    //入隊
    mutating func enqueue(_ newElement:Element)
    //出隊
    mutating func dequeue() -> Element?
}

從這里能夠感受swift特性的冰山一角,這個協(xié)議就三行代碼,協(xié)議越簡單,工作范圍越大,使用越不受約束,效率可能就越低,因此如何使用非常關鍵,Xcode documentation里,swift協(xié)議的文檔有大量的注釋,其中還有很多demo,為的就是給出使用建議。
比如這個Squeue協(xié)議,我們可以建議入隊和出隊的復雜度需要是O(1),這就限制了可以使用這個協(xié)議的數(shù)據(jù)結(jié)構(gòu)。僅僅2個方法,能夠表達的功能太少,比如它沒有peek方法來獲取某個元素,只能按順序出隊,也沒有限制必須是Collection,甚至沒有限制它是FIFO先進先出還是LIFO后進先出等等。

接下來實現(xiàn)隊列

struct FIFOQueue<Element>:Queue{
    
    private var left:[Element] = []
    private var right:[Element] = []
 
    mutating func enqueue(_ newElement: Element) {
        right.append(newElement)
    }
    
    mutating func dequeue() -> Element? {
        if left.isEmpty{
            left = right.reversed()
            right.removeAll()
        }
        return left.popLast()
    }
}

這個實現(xiàn)使用兩個棧 (兩個常規(guī)的數(shù)組) 來模擬隊列的行為。當元素入隊時,它們被添加到 “右”
棧中。當元素出隊時,它們從 “右” 棧的反序數(shù)組的 “左” 棧中被彈出。當左棧變?yōu)榭諘r,再將右
棧反序后設置為左棧。

接下來實現(xiàn)Collection協(xié)議

protocol Collection: Sequence {
    associatedtype Element // inherited from Sequence
    associatedtype Index: Comparable
    associatedtype IndexDistance: SignedInteger = Int
    associatedtype Iterator: IteratorProtocol = IndexingIterator<Self>
where Iterator.Element == Element
    associatedtype SubSequence: Sequence /* ... */
    associatedtype Indices: Sequence = DefaultIndices<Self>
    /* ... */
    var frst: Element? { get }
    var indices: Indices { get }
    var isEmpty: Bool { get }
    var count: IndexDistance { get }
    func makeIterator() -> Iterator
    func prefx(through: Index) -> SubSequence
    func prefx(upTo: Index) -> SubSequence
    func suffx(from: Index) -> SubSequence
    func distance(from: Index, to: Index) -> IndexDistance
    func index(_: Index, offsetBy: IndexDistance) -> Index
    func index(_: Index, offsetBy: IndexDistance, limitedBy: Index) -> Index?
    subscript(position: Index) -> Element { get }
    subscript(bounds: Range<Index>) -> SubSequence { get }
}

Collection協(xié)議有這么多的內(nèi)容,不過很多都在extension中有默認實現(xiàn) ,最終只需要實現(xiàn)下面這些方法

extension FIFOQueue: Collection {
    /// ?個?空集合中?個元素的位置
    public var startIndex: Int { return 0 }
    /// 集合中超過末位的位置---也就是?最后?個有效下標值? 1 的位置
    public var endIndex: Int { return left.count + right.count }
    /// 返回在給定索引之后的那個索引值
    public func index(after i: Int) -> Int { precondition(i < endIndex)
        return i + 1
    }
    /// 訪問特定位置的元素
    public subscript(position: Int) -> Element {
        precondition((0..<endIndex).contains(position), "Index out of bounds")
        if position < left.endIndex {
            return left[left.count - position - 1]
        } else {
            return right[position - left.count]
        }
    }
}

重點在于下標方法 ,left和right兩個數(shù)組加起來保存這完整的隊列元素,需要根據(jù)不同的情況考慮從哪個數(shù)組取數(shù)據(jù),現(xiàn)在來驗證一下

var q = FIFOQueue<String>()
        for x in ["a","b","c","d","e"]{
            q.enqueue(x)
        }
        q.forEach { s in
            print(s)
        }
        print("出隊 \(q.dequeue()!)")
        q.forEach { s in
            print(s)
        }
        print("入隊")
        q.enqueue("f")
        q.forEach { s in
            print(s)
        }
        print("出隊 \(q.dequeue()!)")
        q.forEach { s in
            print(s)
        }

輸出結(jié)果
a
b
c
d
e
出隊 a
b
c
d
e
入隊
b
c
d
e
f
出隊 b
c
d
e
f

實現(xiàn)ExpressibleByArrayLiteral的方法 直接字面量初始化

extension FIFOQueue : ExpressibleByArrayLiteral{
    typealias ArrayLiteralElement = Element
    public init(arrayLiteral elements: ArrayLiteralElement...) {
        left = elements.reversed()
        right = []
    }
}

var q:FIFOQueue = ["a","b","c","d","e"]

索引

索引表示集合中的位置,從上面可以看到,集合有兩個特殊索引,startIndex和endIndex,前者是第一個元素,后者是最后一個元素的下一個位置,也就是endIndex不是有效的下標。
索引一半都是整數(shù),但這不是唯一的選擇,只要具有一定順序,遵循Comparable就可以。
字典也有索引,類型是DictionaryIndex,根據(jù)鍵訪問字典,得到一個value,而通過索引訪問字典,得到一個元組(key: Key, value: Value)
需要注意的是,Collection定義了索引訪問的方法 ,它返回一個非可選的值,哪怕可能會引起crash,因為錯誤的使用索引值,被認為是程序員的錯誤。通過key訪問字典獲取的是一個可選值,而通過DictionaryIndex獲得元組是非可選。
swift的安全檢查選擇了一個合適的范圍,如果所有的API都要考慮失敗,那就太折磨了

集合發(fā)生變化時,索引可能會失效,比如錯位,或者crash,但是字典不太一樣
字典的索引不會隨著新的鍵值對的加入而失效,直到字典的尺寸增大到觸發(fā)重新的內(nèi)存分配。
這是因為一般情況下字典存儲的緩沖區(qū)內(nèi)的元素位置是不會改變的,而在元素超過緩沖區(qū)尺寸
時,緩沖區(qū)會被重新分配,其中的所有元素的哈希值也會被重新計算。從字典中移除元素總是
會使索引失效。

關聯(lián)類型,SubSequence是繼承自Sequence,Collection對SubSequence添加了更詳細的擴展,一個集合類型的 SubSequence 本身也應該是一個 Collection,比如String的關聯(lián)類型SubString

自定義集合索引
作為非整數(shù)索引集合的例子,我們將會構(gòu)建一種在字符串中迭代單詞的方式。當你想要將一個
字符串分割成一個個的單詞,最簡單的方法就是使用
split(separator:maxSplits:omittingEmptySubsequences:) (當然,這是對英文而言),這個方
法將使用提供的分割元素進行分割,把一個集合轉(zhuǎn)變?yōu)槠?SubSequence 的數(shù)組,它有一個缺點:這個方法將會熱心地為你計算出整個數(shù)組。如果你的字符串非常大,而且你只
需要前幾個詞的話,這么做是相當?shù)托У摹榱颂岣咝阅埽覀円獦?gòu)建一個 Words 集合,它能
夠讓我們不一次性地計算出所有單詞,而是可以用延遲加載的方式進行迭代。

實現(xiàn)方案
定義一個從文章中查找單詞的索引方法,首先把開頭的空格都去掉,然后尋找下一個空格作為結(jié)束,最后返回一個Range

extension Substring {
    var nextWordRange: Range<Index> {
        let start = drop(while: { $0 == " "})
        let end = start.firstIndex(where: { $0 == " "}) ?? endIndex
        return start.startIndex..<end
    }
}

struct WordsIndex: Comparable {
    fileprivate let range: Range<Substring.Index>
    fileprivate init(_ value: Range<Substring.Index>) {
        self.range = value
    }
    static func <(lhs: Words.Index, rhs: Words.Index) -> Bool {
        return lhs.range.lowerBound < rhs.range.lowerBound
    }
    static func ==(lhs: Words.Index, rhs: Words.Index) -> Bool {
        return lhs.range == rhs.range
    }
}

struct Words: Collection {
    let string: Substring
    let startIndex: WordsIndex
    init(_ s: String) {
        self.init(s[...])
    }
    private init(_ s: Substring) {
        self.string = s
        self.startIndex = WordsIndex(string.nextWordRange)
    }
    var endIndex: WordsIndex {
        let e = string.endIndex
        return WordsIndex(e..<e)
    }
    
    subscript(index: WordsIndex) -> Substring {
        return string[index.range]
    }
    
    func index(after i: WordsIndex) -> WordsIndex {
        guard i.range.upperBound < string.endIndex else { return endIndex }
        let remainder = string[i.range.upperBound...]
        return WordsIndex(remainder.nextWordRange)
    }
}

collection要求索引訪問元素應該是O(1),如果使用整數(shù)的索引,在獲取第i個的時候,我們需要把整個字符串都處理好,然后取出第i個。
將 Range<Substring.Index> 進行包裝,然后用它來作為索引類型,另外由于Comparable基于Equatable,所以還需要實現(xiàn)==方法
現(xiàn)在使用單詞的range來作為索引訪問words集合,就是O(1)的復雜度,類似于String的索引。

切片

切片已經(jīng)在字符串操作中頻繁使用了,雖然語法有些啰嗦。
所有的集合類型都有切片操作的默認實現(xiàn),并且有一個接受Range<Index> 作為參數(shù)的下標方法。

切片與原集合共享索引,這一點比較違反直覺,但是又覺著合理

let cities = ["New York", "Rio", "London", "Berlin", "Rome", "Beijing", "Tokyo", "Sydney"]
let slice = cities[2...4]
cities.startIndex // 0 
cities.endIndex // 8 
slice.startIndex // 2 
slice.endIndex // 5

如果訪問slice[0],就會crash,因為slice的startIndex是2,沒有0,因此swift不建議使用索引的值(for index in collection.indices)來作為訪問依據(jù),應當盡可能始終選擇for x in collection 的形式。另外如果需要在迭代的時候修改集合的內(nèi)容,可以選擇使用while循環(huán),然后手動在每次迭代的時候增加索引值,這樣你就不會用到 indices 屬性。當你這么做的時候,要記住一定要從collection.startIndex 開始進行循環(huán),而不要把 0 作為開始。

專門的集合類型

1.雙向索引
BidirectionalCollection 在前向索引的基礎上只增加了一個方法,但是它非常關鍵,那就是獲取上一個索引值的 index(before:),用它可以獲取last元素,

extension BidirectionalCollection { 
/// 集合中的最后?個元素。
 public var last: Element? { 
        return isEmpty ? nil : self[index(before: endIndex)] 
    }
 }

BidirectionalCollection 還添加了一些高效的實現(xiàn),比如 suf?x,removeLast 和 reversed。這里的 reversed 方不會直接將集合反轉(zhuǎn),而是返回一個延時加載的表示方式

extension BidirectionalCollection { 
    /// 返回集合中元素的逆序表示?式似乎數(shù)組 
    /// - 復雜度: O(1) 
    public func reversed() -> ReversedCollection<Self> {
         return ReversedCollection(_base: self)
     }
 }

和上面 Sequence 的 enumerated 封裝一樣,它不會真的去將元素做逆序操作。ReverseCollection 會持有原來的集合,并且使用逆向的索引。集合會將所有索引遍歷方法逆序,這樣一來就可以通過前向遍歷來在原來的集合中進行后向遍歷了,相反也一樣。

2.隨機存取的集合類型
RandomAccessCollection 提供了最高效的元素存取方式,它能夠在常數(shù)時間內(nèi)跳轉(zhuǎn)到任意索
引。要做到這一點,滿足該協(xié)議的類型必須能夠 (a) 以任意距離移動一個索引,以及 (b) 測量任
意兩個索引之間的距離,兩者都需要是 O(1) 時間常數(shù)的操作。

3.可變集合支持原地的元素更改
MutableCollection 增加了一個要求,單個元素的下標訪問方法 subscript 現(xiàn)在必須提供一個 setter。
標準庫中很少有滿足 MutableCollection 的類型。在三個主要的集合類型中,只有Array 滿足這個協(xié)議。MutableCollection 允許改變集合中的元素值,但是它不允許改變集合的?度或者元素的順序。后面一點解釋了為什么 Dictionary 和 Set 雖然本身當然是可變的數(shù)據(jù)結(jié)構(gòu),卻不滿足 MutableCollection 的原因。

乍一看讀不懂,實際上就是list[0] = 1這種賦值方法。
給下標訪問添加setter和getter

public subscript(position: Int) -> Element {
  get { 
    precondition((0..<endIndex).contains(position), "Index out of bounds")
     if position < left.endIndex { 
        return left[left.count - position - 1]
     } else { 
        return right[position - left.count]
     } 
  } 
  set { 
    precondition((0..<endIndex).contains(position), "Index out of bounds")
     if position < left.endIndex {
            left[left.count - position - 1] = newValue 
      } else { 
          return right[position - left.count] = newValue 
        }
   }
}

4.添加或者移除元素
對于需要添加或者移除元素的操作,可以使用 RangeReplaceableCollection 協(xié)議。這個協(xié)議有兩個要求:
1.一個空的初始化方法 — 在泛型函數(shù)中這很有用,因為它允許一個函數(shù)創(chuàng)建相同類型的新的空集合。 2.replaceSubrange(_:with:) 方法 — 它接受一個要替換的范圍以及一個用來進行替換的
集合。

extension FIFOQueue: RangeReplaceableCollection { 
    mutating func replaceSubrange<C: Collection>(_ subrange: Range<Int>, with newElements: C) where     C.Element == Element {
     right = left.reversed() + right left.removeAll() right.replaceSubrange(subrange, with: newElements)
    }
 }

這個協(xié)議添加了這些方法
→ append(:) 和 append(contentsOf:) — 將 endIndex..<endIndex (也就是說末尾的空范圍) 替換為單個或多個新的元素。
→ remove(at:) 和 removeSubrange(
:) — 將 i...i 或者 subrange 替換為空集合。
→ insert(at:) 和 insert(contentsOf:at:) — 將 i..<i (或者說在數(shù)組中某個位置的空范圍) 替換為單個或多個新的元素。
→ removeAll — 將 startIndex..<endIndex 替換為空集合

Sequence 和 Collection 協(xié)議構(gòu)成了 Swift 集合類型的基礎,從中理解了所謂面向協(xié)議的冰山一角,高層級的抽象會使模型變得復雜,很多東西不容易看明白,但是越看越覺著很????。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容