逃逸闭包与自动闭包
本教程共 93 篇 · 第 37 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:分清逃逸闭包和非逃逸闭包,知道什么时候必须写
@escaping;理解自动闭包@autoclosure是怎么把普通表达式悄悄变成闭包的。
闭包还有两个容易让人迷糊的属性:它是不是”逃逸”的,以及它是不是”自动”的。这两个词听起来唬人,其实都是在描述闭包的生命周期和创建方式。
1-1 什么是逃逸闭包
当一个闭包作为函数参数传进去,却要等到函数返回之后才被调用,我们就说这个闭包”逃逸”了这个函数。典型例子是异步操作的完成回调:函数启动任务后立刻返回,任务完成时才去调用那个闭包。这种闭包就必须能从函数的作用域里”逃”出去、活到将来。
反过来,如果一个闭包在函数内部就被调用完了、函数返回前它就执行完毕,它就是非逃逸的(nonescaping)。Swift 默认闭包就是非逃逸的,只有你明确想让它逃逸时,才需要加 @escaping 标注。
1-2 用 @escaping 标注
看一个逃逸的例子:闭包被存进函数外面的数组,函数返回后还会被调用。
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
这里闭包被加进 completionHandlers 数组,这个数组在函数外定义,所以函数返回后闭包依然活着。如果你忘了写 @escaping,编译器会直接报错。相反,普通闭包不需要标注:
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
closure() 在函数体内就调用完了,它是非逃逸的,标注 @escaping 反而多余。
Tip记住一句话:闭包被”存起来以后再用”(存数组、存属性、异步回调),就要
@escaping;闭包”当场用完就扔”,不用标。
1-3 逃逸闭包里引用 self 要显式
逃逸闭包有一个坑:当它引用类实例的 self 时,必须显式写出 self,否则编译不过。原因是逃逸闭包容易意外制造循环引用,Swift 强制你写 self,好让你时刻意识到”我正在捕获它”。
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 } // 必须写 self
someFunctionWithNonescapingClosure { x = 200 } // 非逃逸可省略
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x) // 打印 200
completionHandlers.first?()
print(instance.x) // 打印 100
非逃逸闭包里可以隐式用 self,但逃逸闭包一律要求你写出来。如果你愿意,也可以把 self 写进捕获列表:{ [self] in x = 100 },效果相同,但同样是在提醒你”我捕获了它”。
Warning结构体
struct和枚举enum不允许共享可变性。在结构体的mutating方法里,逃逸闭包不能捕获可变的self,编译器会报错。这是值类型的固有规则,不是你写错了。
1-4 自动闭包 @autoclosure 是什么
自动闭包更巧妙。它把一个普通表达式自动包成一个”无参数、调用时返回该表达式值”的闭包。好处是:你调用函数时写的是普通表达式,不用自己写大括号,但函数内部拿到的是一段可以延迟执行的闭包。
核心价值是延迟求值:闭包里的代码在被调用前不会执行。下面这个例子先不用自动闭包:
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) }) // 打印 Now serving Alex!
调用时你还得手写闭包 { ... }。加上 @autoclosure 后,函数参数类型变成 @autoclosure () -> String,调用时直接传表达式:
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0)) // 打印 Now serving Ewa!
看起来像传了个 String,其实是传了个闭包。表达式 customersInLine.remove(at: 0) 要等到 customerProvider() 真正被调用时才执行,这就实现了延迟。标准库里的 assert(condition:) 就用自动闭包:在非调试构建里,condition 根本不会求值,省掉开销。
1-5 @autoclosure 与 @escaping 组合
自动闭包也可以逃逸。同时写两个属性即可:@autoclosure @escaping () -> String。这时闭包既能延迟求值,又能存起来以后调用:
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
闭包被收进数组、函数返回后才逐个执行,所以既要 @escaping 又要 @autoclosure。
Note自动闭包用多了会让代码不好读,因为调用处看不出那是个闭包、也不清楚求值被推迟了。函数名和上下文应当清楚地表明”这里会延迟执行”。
1-6 真实场景与权衡
逃逸闭包最常见的真实身影,是各种”完成回调”(completion handler)。比如发起一个网络请求,函数立刻返回,等数据回来才调用你传进去的闭包。这种闭包必须能活到请求结束之后,所以一定是 @escaping。Swift 之所以默认非逃逸,是为了性能和安全:非逃逸闭包编译器可以做更多优化,也保证它不会偷偷把 self 留下造成循环。只有确实需要”晚点再调”时才打破这个默认。
自动闭包 @autoclosure 则更像一种”语法糖魔术”。它让调用处写起来像传普通值,背后却是段延迟执行的闭包。标准库里 assert(condition:) 就用它:调试构建里才求值 condition,发布构建里直接跳过,省开销。你也可以在自己写的小工具里用它,比如一个 log(_ message: @autoclosure () -> String) 函数,只有真的要打日志时才拼接那段可能很贵的字符串。
但要警惕滥用自动闭包带来的”读不懂”。调用 serve(customer: customersInLine.remove(at: 0)) 看起来平平无奇,其实 remove 被推迟执行了——如果读者不知道参数是 @autoclosure,根本意识不到”这一行有副作用、而且发生时机不确定”。所以社区共识是:自动闭包只用在名字和上下文强烈暗示”我会延迟或按需”的场合,别拿它藏着掖着。
1-7 易错点速记
两个属性各记一句。@escaping:闭包要”活过函数返回”才用,且引用 self 必须显式写出,这是编译器在提醒你”可能成环”。@autoclosure:把表达式包成闭包、延迟求值,调用处看着像普通值,实则会推迟执行——别在语义不明显的函数上滥用,免得读者不知道这里有副作用。判断一个闭包要不要 @escaping,就看它会不会被”存起来以后再用”;会,就标;当场用完,不标。两者还能叠加成 @autoclosure @escaping。
1-8 小结
逃逸闭包用 @escaping 标注,用于”函数返回后才调用”的场景,且引用 self 必须显式写出。自动闭包用 @autoclosure 把普通表达式包成闭包,主要价值是延迟求值。两者还能组合成 @autoclosure @escaping。下一章我们看闭包在日常编码里最常出没的地方——高阶函数。