软件设计

Swift 迭代器模式:Sequence 原理

使用 Swift 开发应用时,我们每天会写几十次类似 for item in array 的循环。由于太过自然,往往要到尝试用 for-in 遍历自定义类型并遇到编译错误后,才会好奇:“这个循环内部到底发生了什么?”

4 分钟阅读
Swift 迭代器模式:Sequence 原理 封面图

使用 Swift 开发应用时,我们每天会写几十次类似 for item in array 的循环。由于太过自然,往往要到尝试用 for-in 遍历自定义类型并遇到编译错误后,才会好奇:“这个循环内部到底发生了什么?”

先说结论,Swift 的 for-in 循环是 SequenceIteratorProtocol 两个协议共同实现的。Sequence 声明“我可以被遍历”,IteratorProtocol 则负责实际取值:“我会提供下一个值。”

本文将梳理两个协议如何分工、for-in 如何在内部展开,以及如何创建自定义序列。如果你觉得迭代器模式很难,请读到最后。

Sequence 和 IteratorProtocol 有什么区别?

先从最容易混淆的地方说起。两者都与遍历有关,但职责完全不同。

IteratorProtocol 只有一个要求:mutating func next() -> Element?。每次调用返回一个元素,没有更多元素时返回 nil。

也就是说,迭代器就是一个“携带状态的游标”。它记住当前读到的位置,每次调用 next() 就前进一格。

Sequence 处于更高层,负责通过 makeIterator() 方法创建并交出迭代器。它相当于告诉你:“想遍历我,就使用这个迭代器。”

总结如下。

  • IteratorProtocol:逐个取值的游标。核心是 next()。
  • Sequence:创建游标的工厂。核心是 makeIterator()。

数组、字典、Set、字符串、Range……Swift 标准库中所有可遍历的类型都遵循 Sequence。


for-in 在内部是如何展开的?

这里是本文的核心。我们随手使用的 for-in,其实是语法糖(syntactic sugar)。

下面是我们平时使用的循环。

for number in [1, 2, 3] {
    print(number)
}

编译器大致会将这段代码转换成下面的形式:用 makeIterator() 创建迭代器,并持续执行 while 循环,直到 next() 返回 nil。

var iterator = [1, 2, 3].makeIterator()
while let number = iterator.next() {
    print(number)  // 1, 2, 3 按顺序输出
}

for-in 并不神奇,归根结底只是 makeIterator() 和 next() 的组合。

原来最终就这两个调用
原来最终就这两个调用

理解这个结构后,就能自然明白为什么只遵循 Sequence 的类型可以直接用于 for-in,因为 for-in 只需要 makeIterator()。


如何创建自定义序列?

亲自实现一次就容易理解了。下面以无限生成 Fibonacci 数列的序列为例。

最简单的方式是在一个小型类型中同时遵循 Sequence 和 IteratorProtocol。这样,makeIterator() 的默认实现就会复制并返回自身。

struct Fibonacci: Sequence, IteratorProtocol {
    var current = 0
    var nextValue = 1
    mutating func next() -> Int? {
        defer { (current, nextValue) = (nextValue, current + nextValue) }
        return current
    }
}

next() 的工作很简单:在返回当前值之前,用 defer 预先计算下一个状态。

现在这个类型可以直接用于 for-in。由于序列是无限的,因此必须手动限制遍历次数。

for n in Fibonacci().prefix(8) {
    print(n)  // 0 1 1 2 3 5 8 13
}
亲自编写 Fibonacci 类型并用于 for-in
亲自编写 Fibonacci 类型并用于 for-in

还可以免费使用 prefix、map、filter 等方法。遵循 Sequence 后,这些标准操作会通过协议扩展自动提供,无需自行实现。


常见问题 Q&A

问:只遵循 Sequence 就够了吗?IteratorProtocol 也必须遵循吗? 答:运行 for-in 最终必须有迭代器。不过像上面的示例一样,如果一个类型同时遵循两个协议,就不必单独编写 makeIterator()。如果想将遍历状态与集合分离,可以把迭代器提取为独立的 struct。

问:遍历过一次的序列,再用 for-in 遍历会怎样? 答:数组等值类型每次都会创建新的迭代器,因此可以安全地遍历多次。但网络流这类消费后就消失的序列只能遍历一次,需要注意。

问:它和 Collection 有什么区别? 答:Collection 是继承 Sequence 的更高层概念,要求具备更多能力,例如通过索引多次访问、统计 count 以及保证顺序。如果只需单向遍历值,Sequence 就足够了。

画出 Sequence、Iterator、Collection 的关系后,一眼就理清了
画出 Sequence、Iterator、Collection 的关系后,一眼就理清了

一行 for-in 背后其实隐藏着 Sequence 和 IteratorProtocol 的清晰分工:迭代器负责游标,序列负责工厂。区分两者后,创建自定义遍历类型就容易多了。

下次使用 for-in 时,不妨想想“现在正在调用 next()”。建议你从亲自创建 Fibonacci 序列开始。

延伸阅读