Swift 中的迭代机制:Sequence、Collection 与 Iterator 完全拆解

前言

日常开发里,我们写 for item in list 像呼吸一样自然。

但 Swift 编译器在背后悄悄做了三件事:

  1. 调用 list.makeIterator() 拿到一个迭代器
  2. 反复调用 iterator.next()
  3. 把返回的可选值解包后赋给 item

一旦理解这三步,你就能

  • 自己写"能 for-in 的数据结构"
  • 避免"遍历同时修改"导致的崩溃
  • 把回调式 API 优雅地转成 AsyncSequence

Sequence:最小迭代单元

协议定义(核心部分,Swift 5.9 仍不变)

swift 复制代码
public protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    
    func makeIterator() -> Iterator
}

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

关键知识点

  1. Sequence 只承诺"能生成迭代器",不保证能反复遍历,也不保证有 count
  2. 迭代器几乎总是 struct:值语义保证"复制一份就从头开始",不会意外共享状态。
  3. 单趟序列(例如网络流)完全合法;第二次 makeIterator() 可以返回空迭代器。

代码示例:自定义一个"从 n 倒数到 0"的序列

swift 复制代码
struct Countdown: Sequence {
    let start: Int
    
    // 每次 for-in 都会调用一次,生成新的迭代器
    func makeIterator() -> Iterator {
        Iterator(current: start)
    }
    
    struct Iterator: IteratorProtocol {
        var current: Int
        
        // 返回 nil 时代表迭代结束
        mutating func next() -> Int? {
            guard current >= 0 else { return nil }
            defer { current -= 1 }          // 先返回,再减
            return current
        }
    }
}

// 使用
for number in Countdown(start: 3) {
    print(number)   // 3 2 1 0
}

Collection:在 Sequence 上加了三把锁

Collection 额外保证

  • 可多次遍历且顺序稳定(除非自己把文档写错)
  • 提供 countendIndex、下标访问
  • 支持切片、前缀、后缀等默认实现

协议片段

swift 复制代码
public 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
}

因为多趟安全,mapfilter 可以提前分配内存;

因为下标存在,ArrayDictionarySet 都直接 conform。

for-in 的糖衣剥开长这样

编译器把

swift 复制代码
for element in container {
    print(element)
}

翻译成

swift 复制代码
var iterator = container.makeIterator()
while let element = iterator.next() {
    print(element)
}

理解这段模板代码,你就能:

  • 在 Playground 里手动模拟 for 循环
  • 把"遍历同时修改"的崩溃场景复现出来

遍历同时修改:崩溃现场与三种安全写法

现场:遍历数组时删除元素

swift 复制代码
var todoItems = ["A", "B", "C"]

// 目前倒是没有崩溃,但是也不是很符合逻辑
for (index, item) in todoItems.enumerated() {
    if item == "B" {
        todoItems.remove(at: index)   // ❌ Fatal error: Collection modified while enumerating
    }
}

原因:数组缓冲区搬迁,迭代器指针失效。

三种安全写法:

  1. 官方一次性 API
swift 复制代码
todoItems.removeAll { $0 == "B" }
  1. 先记下索引,后删除
swift 复制代码
let indexesToRemove = todoItems.indices.filter { todoItems[$0] == "B" }
for i in indexesToRemove.reversed() {
    todoItems.remove(at: i)
}
  1. 过滤后整体替换
swift 复制代码
todoItems = todoItems.filter { $0 != "B" }

AsyncSequence:把"迭代"搬到异步世界

协议定义

swift 复制代码
public protocol AsyncSequence {
    associatedtype Element
    associatedtype AsyncIterator: AsyncIteratorProtocol where AsyncIterator.Element == Element
    func makeAsyncIterator() -> AsyncIterator
}

public protocol AsyncIteratorProtocol {
    associatedtype Element
    mutating func next() async throws -> Element?
}

消费方式

swift 复制代码
for await element in stream {
    print(element)          // 会在每次 next() 挂起时让出线程
}

桥接回调式 API 的模板:进度条场景

swift 复制代码
func makeProgressStream() -> AsyncStream<Double> {
    AsyncStream { continuation in
        let token = ProgressCenter.onUpdate { value in
            continuation.yield(value)
            if value >= 1.0 { continuation.finish() }
        }
        continuation.onTermination = { _ in
            ProgressCenter.removeObserver(token)
        }
    }
}

// 使用
Task {
    for await p in makeProgressStream() {
        progressView.progress = Float(p)
    }
}

自己动手:一个固定容量的 RingBuffer

需求:保持最新 N 条日志,支持 for-in 打印。

swift 复制代码
struct RingBuffer<Element>: Collection {
    private var storage: [Element?]   // 用 Optional 占位
    private var head = 0
    private var tail = 0
    private(set) var count = 0
    private let capacity: Int
    
    init(capacity: Int) {
        self.capacity = capacity
        storage = Array(repeating: nil, count: capacity)
    }
    
    // 写入新元素,覆盖最旧数据
    mutating func append(_ newElement: Element) {
        storage[tail] = newElement
        tail = (tail + 1) % capacity
        if count == capacity {
            head = (head + 1) % capacity   // 丢弃最旧
        } else {
            count += 1
        }
    }
    
    // MARK: Collection 必备
    typealias Index = Int
    
    var startIndex: Int { 0 }
    var endIndex: Int { count }
    
    func index(after i: Int) -> Int {
        precondition(i < endIndex, "Index out of bounds")
        return i + 1
    }
    
    subscript(position: Int) -> Element {
        precondition((0..<count).contains(position), "Index out of bounds")
        let offset = (head + position) % capacity
        return storage[offset]!
    }
}

// 使用
var buffer = RingBuffer<Int>(capacity: 3)
for i in 1...5 {
    buffer.append(i)   // 1,2,3 → 2,3,4 → 3,4,5
}

for value in buffer {
    print(value)   // 3 4 5
}

总结与扩展场景

  1. 协议层次

IteratorProtocolSequenceCollectionBidirectionalCollectionRandomAccessCollection

每一层只加必要约束,绝不多要一颗糖。

  1. 值语义是 Swift 迭代的灵魂

结构体迭代器复制即"新游标",避免共享状态,这点与 Objective-C 的 NSEnumerator 形成鲜明对比。

  1. 遍历同时修改的崩溃本质是"迭代器失效"

所有带指针/索引的集合都存在,掌握"先记录后改"或"一次性 API"即可。

  1. AsyncSequence 让"事件流"变成普通 for-in

网络下载、蓝牙数据、用户点击序列都能用同一套思维建模;配合 AsyncStream 几乎零成本桥接老代码。

  1. 自定义 Collection 是架构试金石

RingBuffer 这类小容器写一遍,你会深刻理解"下标换算"、"容量与 count 区别"、"前置条件断言"这些日常被标准库隐藏的细节。

学习资料

  1. www.donnywals.com/a-deep-dive...
相关推荐
HarderCoder8 小时前
告别并发警告:Swift 6 线程安全通知 MainActorMessage & AsyncMessage 实战指南
swift
HarderCoder8 小时前
【SwiftUI 任务身份】task(id:) 如何正确响应依赖变化
swift
非专业程序员9 小时前
精读GitHub - swift-markdown-ui
ios·swiftui·swift
5***79001 天前
Swift进阶
开发语言·ios·swift
大炮走火1 天前
iOS在制作framework时,oc与swift混编的流程及坑点!
开发语言·ios·swift
0***142 天前
Swift资源
开发语言·ios·swift
z***I3942 天前
Swift Tips
开发语言·ios·swift
J***Q2922 天前
Swift Solutions
开发语言·ios·swift
Gavin-Wang2 天前
Swift + CADisplayLink 弱引用代理(Proxy 模式) 里的陷阱
开发语言·ios·swift