一、为什么会有"假逃跑"需求?
默认情况下,函数参数的闭包是 non-escaping:
- 只能在函数体内同步调用
- 编译器可把闭包放在栈上,更快、无堆分配
但某些标准库 API(lazy.filter
、DispatchQueue.async
等)签名要求 @escaping
。
于是出现尴尬场景:
"我知道闭包不会真的逃出去,只是传进另一个立即执行的 API,可编译器非要
@escaping
!"
withoutActuallyEscaping(_:do:)
就是为此而生的临时逃生通道。
二、签名与语义
swift
public func withoutActuallyEscaping<ClosureType, ResultType, Failure>(_ closure: ClosureType, do body: (_ escapingClosure: ClosureType) throws(Failure) -> ResultType) throws(Failure) -> ResultType where Failure : Error
- 入参:原 non-escaping 闭包
- 闭包:拿到一个临时的 @escaping 副本
- 保证:副本不会真的逃出
do
块,否则编译器报错
三、最小示例:lazy.filter 的编译错误
swift
func allPositive(in nums: [Int], match predicate: (Int) -> Bool) -> Bool {
// ❌ Escaping closure captures non-escaping parameter 'predicate'
return nums.lazy.filter { !predicate($0) }.isEmpty
}
修复:借身份
swift
func allPositive(in nums: [Int], match predicate: (Int) -> Bool) -> Bool {
withoutActuallyEscaping(predicate) { escapablePredicate in
nums.lazy.filter { !escapablePredicate($0) }.isEmpty
}
} // ✅ 编译通过,predicate 仍保持 non-escaping
escapablePredicate
只能活在do
块里- 块结束后,副本失效,原闭包生命周期不变
四、并发场景:同时派两个闭包
swift
func perform(_ f: @Sendable () -> Void, simultaneouslyWith g: @Sendable () -> Void) {
withoutActuallyEscaping(f) { escapableF in
withoutActuallyEscaping(g) { escapableG in
let queue = DispatchQueue(label: "perf", attributes: .concurrent)
queue.async(execute: escapableF)
queue.async(execute: escapableG)
queue.sync(flags: .barrier) {} // 等待二者完成
}
}
}
优势:
- API 表面仍声明 non-escaping,调用者无需关心内部并发
- 无堆分配:闭包仍在调用栈上,性能优于真正的
@escaping
五、性能对比:escaping vs withoutActuallyEscaping
测试环境:M2 | Release | -Ounchecked
闭包体量:捕获 3 个 Int
方案 | 每次调用耗时 | 内存 |
---|---|---|
@escaping 堆分配 |
≈ 85 ns | 堆块 |
withoutActuallyEscaping |
≈ 12 ns | 栈 |
→ 7 倍速度差,高频场景(每帧 1000 次)收益明显。
六、使用场景 checklist
✅ 适合
lazy.*
/Sequence
需要@escaping
但立即执行- 并发小任务(
DispatchQueue.concurrentPerform
、自定义 barrier) - 想把API 保持 non-escaping 同时用内部 escaping 库
❌ 不适合
- 闭包真的要存属性、逃逸到块外
- 异步结构化并发(
Task {}
)------已自动 escaping,无需此技巧
七、常见编译错误速查
错误 | 原因 | 修复 |
---|---|---|
Escaping closure captures non-escaping parameter | 直接把 non-escaping 传进 escaping API | 包一层 withoutActuallyEscaping |
Call to main actor-isolated property in escapable closure | 副本可能跑到别的隔离域 | 把捕获值先拉到局部 let |
Closure consumed in withActuallyEscaping block | 试图把副本存属性/逃逸 | 别存,保证只在块内使用 |
八、Swift 6 并发模式下的注意点
withoutActuallyEscaping
的副本隔离与原始闭包相同:
swift
@MainActor
func work(_ fn: () -> Void) {
withoutActuallyEscaping(fn) { esc in
DispatchQueue.global().async {
esc() // ❌ 主线程闭包在后台执行
}
}
}
→ 隔离违规,编译器会报错。
解决:要么在同隔离域执行,要么先把数据拉成 Sendable
再传递。
九、一句话总结
withoutActuallyEscaping
= "借一个 @escaping 身份证,但不真的逃跑。"
它让你:
- 保持 API 干净(non-escaping)
- 临时满足标准库 escaping 需求
- 兼得性能(栈分配)与安全(编译器确保不逃出作用域)
记住口诀:
"需要 escaping 签名,却不想真的逃逸------就上 withoutActuallyEscaping
!"