3、异步序列(AsyncSequence):AsyncSequence协议、for-await-in循环、自定义异步序列

好,咱们来聊聊异步序列。说实话,这个特性刚出来的时候,我第一反应是:「这不就是异步版的数组吗?」后来在项目里真正用上了,才发现自己太天真了。它远比数组要灵活,也更容易踩坑。

3.1 AsyncSequence 协议

先看定义。AsyncSequence 是 Swift 并发编程里的一个核心协议。它描述了一种可以异步产生一系列值的类型。你可以把它想象成一个水管,数据是一滴一滴流出来的,而不是一次性倒给你一桶。

核心要点:AsyncSequence 与 Sequence 最大的区别在于,它的 next() 方法是 async 的。也就是说,每次获取下一个元素,都可能需要等待。

协议本身很简单,只有一个必需的方法:

public protocol AsyncSequence {
    associatedtype AsyncIterator: AsyncIteratorProtocol
    func makeAsyncIterator() -> AsyncIterator
}

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

嗯,这里要注意。next() 方法返回的是可选值。当序列结束时,它返回 nil。这和同步的 IteratorProtocol 是一个道理。

我在项目中遇到过这样一个场景:从网络分页加载用户列表。如果用传统的回调方式,代码会嵌套得很深。用 AsyncSequence 就清爽多了——每次翻页就是一次 next() 调用。

3.2 for-await-in 循环

有了 AsyncSequence,怎么消费它呢?Swift 提供了 for-await-in 语法。说白了,就是异步版的 for-in。

for try await item in asyncSequence {
    // 处理每个 item
}

你想想看,这个语法有多自然。以前我们写网络请求,要么用回调,要么用 Combine。现在直接一个循环搞定。

举个例子。假设我们要读取一个超大文件,逐行处理:

let url = URL(fileURLWithPath: "/path/to/large/file.txt")
let lines = url.lines

for try await line in lines {
    // 处理每一行
    print(line)
}

这里有个细节。lines 属性返回的是一个 AsyncLineSequence。它不会一次性把整个文件读进内存,而是逐行读取。这对内存友好极了。

我的习惯:在处理流式数据时,我通常会加上超时控制。因为 for-await-in 会一直等待,如果数据源出了问题,可能会卡住。可以用 withTimeout 或者 Task 的取消机制来兜底。

为什么会这样?因为 for-await-in 本质上是在一个异步上下文中执行的。如果序列永远不会结束,循环就不会退出。我曾经在一个聊天应用里遇到过这个问题——服务器连接断了,但客户端还在傻等。嗯,从那以后我养成了加超时的习惯。

3.3 自定义异步序列

光会用还不够,咱们还得会造。自定义 AsyncSequence 其实不复杂,但有几个坑要注意。

先看一个简单的例子。实现一个计数器,每秒递增一次:

struct Counter: AsyncSequence {
    typealias Element = Int
    
    let limit: Int
    
    func makeAsyncIterator() -> CounterIterator {
        CounterIterator(limit: limit)
    }
}

struct CounterIterator: AsyncIteratorProtocol {
    let limit: Int
    var current = 0
    
    mutating func next() async -> Int? {
        guard current < limit else { return nil }
        
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        defer { current += 1 }
        return current
    }
}

使用起来很简单:

let counter = Counter(limit: 5)
for await value in counter {
    print(value) // 每秒打印 0, 1, 2, 3, 4
}

这里有个关键点。next() 方法必须是 mutating 的。因为你要修改迭代器的内部状态。我刚开始写的时候忘了加 mutating,编译器报错我还纳闷了半天。

避坑指南:我曾经在自定义 AsyncSequence 时,把迭代器设计成了引用类型(class)。结果多个 Task 同时迭代同一个序列,状态全乱了。记住,迭代器应该是值类型(struct),每个 for-await-in 循环都会创建自己的迭代器副本。

再来看一个更实际的例子。模拟一个分页加载的序列:

struct PaginatedData<T>: AsyncSequence {
    typealias Element = [T]
    
    let fetchPage: (Int) async throws -> [T]
    
    func makeAsyncIterator() -> PaginatedIterator<T> {
        PaginatedIterator(fetchPage: fetchPage)
    }
}

struct PaginatedIterator<T>: AsyncIteratorProtocol {
    let fetchPage: (Int) async throws -> [T]
    var page = 0
    var hasMore = true
    
    mutating func next() async throws -> [T]? {
        guard hasMore else { return nil }
        
        let items = try await fetchPage(page)
        page += 1
        hasMore = !items.isEmpty
        return items
    }
}

使用方式:

let paginated = PaginatedData { page in
    // 模拟网络请求
    try await Task.sleep(nanoseconds: 500_000_000)
    return Array(0..<10).map { "Page\(page)-Item\($0)" }
}

for try await page in paginated {
    print("收到一页数据: \(page)")
}

我个人习惯把这种分页序列封装成工具类。项目里多个地方都能复用。你想想看,以前用回调实现分页,得维护一个 page 变量,还得处理各种边界情况。现在一个 AsyncSequence 全搞定了。

3.4 常用操作符

AsyncSequence 也支持类似 Combine 的操作符。不过目前标准库提供的还不多,主要依赖 AsyncAlgorithms 这个官方库。

操作符 说明 示例
map 转换每个元素 asyncSeq.map { $0 * 2 }
filter 过滤元素 asyncSeq.filter { $0 > 0 }
prefix 取前 N 个元素 asyncSeq.prefix(5)
merge 合并多个序列 merge(seq1, seq2)
debounce 防抖 asyncSeq.debounce(for: .seconds(1))

举个例子,用 map 和 filter 处理流式数据:

let numbers = Counter(limit: 10)
let processed = numbers
    .filter { $0 % 2 == 0 }
    .map { "偶数: \($0)" }

for await value in processed {
    print(value)
}

这里要注意,这些操作符返回的也是 AsyncSequence。所以你可以链式调用,就像操作普通数组一样自然。

我的建议:如果项目里频繁用到 AsyncSequence 的操作符,建议直接引入 AsyncAlgorithms 库。它提供了 50 多个操作符,基本覆盖了日常需求。别自己造轮子,除非你有特殊需求。

3.5 实战:文件监控

最后分享一个我在项目中用过的例子。监控一个文件的变化,实时读取新增内容:

struct FileMonitor: AsyncSequence {
    typealias Element = String
    
    let url: URL
    
    func makeAsyncIterator() -> FileMonitorIterator {
        FileMonitorIterator(url: url)
    }
}

struct FileMonitorIterator: AsyncIteratorProtocol {
    let url: URL
    var fileHandle: FileHandle?
    var position: UInt64 = 0
    
    init(url: URL) {
        self.url = url
        self.fileHandle = try? FileHandle(forReadingFrom: url)
        self.position = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
    }
    
    mutating func next() async -> String? {
        while true {
            guard let handle = fileHandle else { return nil }
            
            try? await Task.sleep(nanoseconds: 500_000_000)
            
            try? handle.seek(toOffset: position)
            let data = try? handle.readToEnd()
            
            guard let data = data, !data.isEmpty else {
                continue
            }
            
            position += UInt64(data.count)
            return String(data: data, encoding: .utf8)
        }
    }
}

使用方式:

let monitor = FileMonitor(url: URL(fileURLWithPath: "/tmp/test.log"))
for await line in monitor {
    print("新内容: \(line)")
}

嗯,这个例子虽然简单,但很实用。我曾经用它来实时监控服务器日志,配合 UI 更新,效果非常好。

总结一下。AsyncSequence 是 Swift 并发编程里一个非常强大的工具。它把异步数据流抽象成了一个统一的接口,让代码更简洁、更易读。自定义 AsyncSequence 也不难,只要记住迭代器是值类型、next() 是 mutating 的、返回 nil 表示结束,基本就不会出大问题。

下一章咱们聊聊 Actor,那才是真正的并发安全利器。