Swift 5.5 引入的异步序列(AsyncSequence)让我们能够用for-await-in循环优雅地消费异步产生的数据流。它与同步的Sequence一样,支持map、filter、reduce等操作,其中scan是一个常用但容易被忽视的高阶函数。扫描操作会维护一个累积值,对序列中的每个元素执行一次变换,并将累积结果传递到下一次调用,最后输出每一步的累积值。当处理可能出错的异步流时,普通的scan无法在闭包中直接抛出错误并终止序列,这时AsyncThrowingScanSequence便成了不可或缺的工具。本文将深入探讨如何在 Swift 中构建带错误处理的扫描操作,让累加逻辑既能逐步推进,又能在失效时安全退出。

从普通 scan 到可抛错的 AsyncThrowingScanSequence
AsyncSequence协议提供了scan(_:_:)方法,它接收一个初始值和一个闭包,闭包每次被调用时会拿到当前累积值和序列元素,返回新的累积值。例如,我们可以将一个异步产生整数的流累加求和:
let sumSequence = numberSequence.scan(0) { accumulated, element in
accumulated + element
}
for try await sum in sumSequence {
print(sum)
}上述写法简单明了,但闭包是non-throwing的。如果数据源可能产生非法值,比如我们需要在累加过程中检查元素是否合法,一旦不合法就应当中断整个序列,这时scan就无能为力了。我们可以在闭包内部使用try?吞掉错误,但无法让上游的for-await-in感知到终止原因。这正是AsyncThrowingScanSequence解决的问题:它来自AsyncSequence的throwingScan(_:_:)方法,允许闭包抛出错误,并且这个错误会传播给序列的消费者。
throwingScan返回一个AsyncThrowingScanSequence类型的序列,它自身也遵循AsyncSequence。其元素类型是闭包返回值,但迭代时需要用try await来捕获可能抛出的错误。这种方法将错误处理与扫描逻辑合二为一,避免了在异步流处理中编写繁琐的do-catch嵌套。例如,当你从一个异步网络流中解析数值并累加时,可能会遇到格式错误,此时抛出错误即可立即中止并通知调用方,而无需借助额外的状态变量或返回Optional来暗示失败。下面,我们将通过一个完整的示例演示如何用AsyncThrowingScanSequence构建带校验的累加器。
实战:构建一个可抛错的异步扫描序列
假设我们有一个异步序列,每个元素代表一次交易金额,我们需要计算累计余额,但如果遇到负金额则认为是无效数据,需要中断扫描并抛出错误。这种模式在金融数据校验、实时日志分析等场景中十分常见:你希望持续累加合法数据,一旦出现异常就立即拒绝剩余数据,并保留错误上下文以便问题追溯。首先,我们可以通过AsyncStream制造一个测试用的异步序列:
func transactionStream() -> AsyncStream<Int> {
AsyncStream { continuation in
let amounts = [100, 200, -50, 150]
for amount in amounts {
continuation.yield(amount)
}
continuation.finish()
}
}这个函数模拟了一个持续输出金额的异步流,其中包含了会引起错误的负值。接下来,我们使用throwingScan构建余额累加器,并在闭包中插入校验逻辑:
enum TransactionError: Error {
case invalidAmount(Int)
}
let balanceSequence = transactionStream().throwingScan(0) { balance, amount in
guard amount >= 0 else {
throw TransactionError.invalidAmount(amount)
}
return balance + amount
}这里我们把验证逻辑直接写在scan闭包里,不满足条件的amount会立即抛出TransactionError。一旦抛出错误,throwingScan内部的迭代会立即结束,后续的任何元素都不会再被处理。消费这个序列时,for-await-in循环会被错误中断:
do {
for try await balance in balanceSequence {
print("当前余额: \(balance)")
}
} catch {
print("扫描错误: \(error)")
}运行这段代码,前两次合法交易会打印出100、300,当处理到-50时,序列抛出错误,循环结束并打印出扫描错误。这种模式特别适合解析有错误边界的流式数据,比如 JSON 流、自定义协议包等,你可以在解析每一个对象时做完整性校验,一发现畸变就马上终止并上报,而下游只需一个try catch即可集中管理所有失败场景,无需在每个环节分散捕获。
AsyncThrowingScanSequence 与普通 scan 的错误处理区别
理解throwingScan和scan的核心差异,有助于在正确的场景下选用正确的工具。普通的scan要求闭包不抛出检查,这意味着所有错误必须内化处理。比如你可能会采用产生nil或哨兵值的方式:
let safeSequence = numberSequence.scan(0) { acc, element in
guard element != 0 else { return acc } // 简单跳过,但不停止序列
return acc + element
}这种做法的问题是:消费者无法区分“序列正常结束了”还是“因为错误被跳过了”。在需要严格失败语义的异步流(如网络帧解码器)中,这可能导致后续逻辑基于错误数据继续执行,甚至将错误状态传递到完全无关的模块。AsyncThrowingScanSequence则把错误上升为序列终止事件,遵循“快速失败”原则,保证流一旦污染就不再传播,这样任何依赖该流的业务都能一致地通过try await感知到失败。
然而,使用throwingScan也要注意,一旦抛出错误,序列就不可恢复了。如果你需要在某些错误后恢复扫描,就需要在闭包中捕获错误并以某种标记值继续,但这样又会陷入无法区分错误的问题。对于更复杂的场景,可以考虑结合flatMap或自定义AsyncSequence类型,把错误转变为可选类型推向下游,同时保留错误信息。但在最清晰的“全有或全无”语义下,AsyncThrowingScanSequence是最纯粹的方案。特别地,当你的异步流上游可能是网络连接或文件读取等不可重试的资源时,快速失败能够防止因数据损坏而产生的雪崩式逻辑错误。
另外,性能层面两者差异很小,因为底层迭代机制相同,只是throwing版本会额外传递错误路径。对绝大多数应用来说,这种开销可以忽略不计。
常见误用与最佳实践
新手在使用AsyncThrowingScanSequence时常犯两种错误。第一是在闭包中误吞错误。下面这段代码看起来无害,但实际上隐藏了错误信息:
let badSequence = stream.throwingScan(0) { acc, element in
do {
return try someOperation(element) + acc
} catch {
return acc // 错误被静默丢弃
}
}这样写会让throwingScan本身失去意义,因为闭包永远不会抛出。正确的做法是当遇到不可恢复的错误时直接throw,让消费者处理;如果错误是可恢复的,则应该在序列外或通过combine机制解决,而不是在scan里遮掩。假如你确实需要在局部处理某些特定错误,而将其他错误继续向上抛,也可以在catch块中重新throw,但务必保证有意为之,避免吞噬未知错误导致调试困难。
第二是忽视取消协作。在异步序列迭代过程中,若外部任务被取消,扫描闭包可能已部分执行。虽然throwingScan本身不自动响应CancellationError之外的异步抛出,但闭包内的检查点可以通过Task.checkCancellation()手动响应取消。建议在长时间运行的scan闭包内添加取消检查,例如在循环处理大量数据时定期调用try Task.checkCancellation(),以释放系统资源并避免不必要的工作。当序列在结构化并发中被取消时,这一习惯能显著提升应用的响应性和资源利用率。
当需要针对不同的错误类型实施特定恢复策略时,可以在消费序列的do-catch块中分支处理,也可以在throwingScan返回的序列上使用与其他AsyncSequence组合的catch操作符(如果可用)。但需注意,一旦扫描序列抛出错误,整个序列即终止,无法像Combine那样中途替换上游。因此,若业务要求从错误中自动恢复并继续接收新的元素,你应该在更外层的逻辑中重新构建并订阅一个新的异步序列。
总结
AsyncThrowingScanSequence为异步数据流的累加扫描提供了一条干净的错误出口。它不需要额外包装,直接在scan闭包中throw即可终止序列,把错误交给下游的try await捕获。通过本文的示例,我们看到了如何用它进行带校验的金额累加,并分析了它与普通scan的不同以及常见误区。在实际开发中,当你需要对异步序列的每一次产物进行累积运算,同时又希望在有数据异常时立刻失败,让调用方明确感知并处理错误,AsyncThrowingScanSequence就是最契合 Swift 并发模型的选择。掌握它,能让你的异步流处理逻辑更加健壮、意图更加清晰。
AsyncSequenceAsyncThrowingScanSequence异步序列错误处理修改时间:2026-08-12 07:45:47