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,那才是真正的并发安全利器。