说说 Swift 中的集合协议--Pt.1

615 阅读4分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战


引言

在 Swift 中,有一块内容,我一直都不太愿意去学,那就是全系列的集合类型。那现在为啥去看了呢,因为卷 主要原因是标准库中提供的具体实现--数组、集合和字典--能覆盖日常开发 99% 的情况。还有一个更重要的原因----协议太多了!

更不用说 Swift 5.5 了。介绍了一个全新的基于 AsyncSequence 的协议家族,不过这篇文章中不会涉及。

那就让我们试着弄清楚每个协议的作用,以及为什么我们需要这么多协议。下面的图就展示了全系列的集合协议,我将从上到下依次介绍:

Collection协议.png


IteratorProtocol

一次提供一个序列值的类型。该协议与 Sequence 协议紧密相连,后面就会看到。序列通过创建迭代器来提供对其元素的访问,迭代器跟踪其迭代过程,并随着序列的前进每次返回一个元素。协议的定义如下:

public protocol IteratorProtocol {
    associatedtype Element
    mutating func next() -> Element?
}

一个名为 next() 的方法,返回下一个元素或 nil。它被标记为 mutating,这样就可以更新他们的内部状态,为下一次调用 next() 做准备。如果实现没有返回 nil,迭代器可以无限地生成值。我们很少直接创建迭代器,因为 Swift 中的序有一种更为常用的方法。不过,让我们创建一个具体的迭代器来看看它是如何工作的。

struct DoublingIterator: IteratorProtocol {
    var value: Int
    var limit: Int? = nil

    mutating func next() -> Int? {
        if let l = limit, value > l {
            return nil
        } else {
            let current = value
            value *= 2
            return current
        }
    }
}

var doublingIterator = DoublingIterator(value: 1, limit: 1024)
while let value = doublingIterator.next() {
    print(value)
}

调用一个简单的迭代器,每次调用 next(),值增加一倍。如果我们初始化 DoublingIterator 时,limit 传 nil,迭代器将永远运行,直到值超出最大值。


Sequence

可以用对其元素进行顺序迭代访问的类型。序列就是一系列的值,每次我们都可以访问一个。虽然看起来很简单,但是这种能力使我们能够进行大量的操作,而且可以对任何序列执行这些操作。Sequence 协议为这些常见操作提供了缺省实现。在研究这些操作之前,让我们先看一下协议定义~

public protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    func makeIterator() -> some IteratorProtocol
}

序列通过它的 makeIterator() 方法和关联的类型来确保迭代器的 Element 类型和序列匹配。让我们使用 DoublingIterator 创建一个具体的 DoublingSequence:

struct DoublingSequence: Sequence {
    var value: Int
    var limit: Int? = nil

    func makeIterator() -> DoublingIterator {
        DoublingIterator(value: value, limit: limit)
    }
}

let doubler = DoublingSequence(value: 1, limit: 1024)
for value in doubler {
    print(value)
}

print(doubler.contains { $0 == 512 }) // true
print(doubler.reduce(0, +)) // 2047

仅仅通过遵循 Sequence,我们的具体类型就获得了 for-in 和一些其他操作的能力,如 map、 filter、 reduce 等。Sequence 还提供了 dropFirst(_:)、 dropLast(_:) 等方法。但是,在序列级别,这些方法的实现受到一次迭代一个元素的约束。这反映了它们的时间复杂度—— dropFirst(_:)是 O(k),其中 k 是要删除的元素数,dropLast(_:) 是 O(n),其中 n 是按顺序排列的元素总数。dropLast(_:)dropFirst(_:)不同,它要求序列是有限的。

在使用 Sequences 时需要注意一些事情

  • 序列不能保证多次迭代产生想要的结果。由实现类型决定如何处理对已经遍历过一次的序列的迭代
  • 序列提供的迭代器应保证时间复杂度为 O(1)。它对元素访问没有其他要求。因此,除非另有文档说明,否则应该将遍历序列的方法视为 O(n)

给定一个元素,Sequence 允许我们移动到下一个元素。为了能够移动到任何元素(虽然不能保证移动的世界是常量时间),我们需要 Collection。


Collection

一个序列,其元素可以通过下标被多次访问。当我们使用数组、字典或集合时,受益于 Collection 协议声明和实现的操作。除了从 Sequence 协议继承的操作之外,我们还可以访问集合中特定位置的元素的方法。Collection 的协议定义如下:

protocol Collection: Sequence {
    associatedtype Index: Comparable

    var startIndex: Index { get }
    var endIndex: Index { get }
    subscript(position: Index) -> Element { get }
    func index(after i: Index) -> Index
}

由于多次遍历和通过索引下标访问的需要,一个集合不能延迟地计算它的值,也不能是无限的。这与 Sequences 不同,Sequences 可以通过当前对 next() 的调用更新内部状态,为下一次调用做准备。还要注意,associatedtype Index 不是 Int 类型,而是符合 Comparable 的任何类型。


结语

在下篇文章中,我们继续探索剩余的部分~