前言
闭包是 Swift 的"灵魂语法"之一。它同时承担了
- 函数式编程的高阶函数;
- 面向对象中的委托回调;
- 异步并发中的逃逸闭包;
- 甚至属性包装器与 DSL 的构建基础。
闭包到底是什么?------ 一句话定义
闭包是自包含的代码块,可以在代码里被传递、被调用,同时自动捕获其定义时所在上下文中的常量和变量。
Swift 的闭包有三种形态:
- 全局函数:有名字,不捕获任何值。
- 嵌套函数:有名字,可捕获外层函数局部量。
- 闭包表达式:无名字,轻量级语法,可捕获上下文。
闭包表达式语法拆解
最简形式:
swift
{ (parameters) -> returnType in
statements
}
下面用"反向排序"一例,把 5 次迭代全部还原
swift
// 0. 原始数组
let names = ["Chris", "Alex", "Ewa", "Barry", "Dani"]
// 1. 最原始:写一个普通函数
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2 // 按字母降序
}
let reversed1 = names.sorted(by: backward)
print(reversed1) // ["Ewa", "Dani", "Chris", "Barry", "Alex"]
// 2. 第一次简化:写成完整闭包表达式
let reversed2 = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
print(reversed2)
// 3. 第二次简化:类型推断,省略参数/返回类型
let reversed3 = names.sorted(by: { s1, s2 in return s1 > s2 })
print(reversed3)
// 4. 第三次简化:单表达式可省 return
let reversed4 = names.sorted(by: { s1, s2 in s1 > s2 })
print(reversed4)
// 5. 第四次简化:使用 $0、$1 占位符
let reversed5 = names.sorted(by: { $0 > $1 })
print(reversed5)
// 6. 第五次简化:直接传运算符
let reversed6 = names.sorted(by: >)
print(reversed6)
Trailing Closure ------ 尾随闭包
当闭包是最后一个参数且较长时,可写在调用括号外,增强可读性。
单参数可省括号;多参数时,第一个尾随闭包可省标签,其余必须带标签。
swift
// 0. 原始数组
let names = ["Chris", "Alex", "Ewa", "Barry", "Dani"]
// 单参数,括号直接省
let reversed7 = names.sorted { $0 > $1 }
class Server {}
class Picture {}
let server = Server()
func show(_ picture: Picture) {
print("一张图片")
}
func show(_ e: Error) {
print("一个错误")
}
// 多参数:loadPicture
func loadPicture(
from server: Server,
completion: @escaping (Picture) -> Void,
onFailure: @escaping (Error) -> Void
) { /* 网络代码 */ }
loadPicture(from: server) { picture in
show(picture)
} onFailure: { error in
show(error)
}
捕获值(Capturing Values)------ 闭包的"黑魔法"
嵌套函数/闭包表达式可以延长局部变量的生命周期。
swift
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0 // 外层局部变量
func incrementer() -> Int { // 嵌套函数
runningTotal += amount // 捕获两个量
return runningTotal
}
return incrementer
}
let inc10 = makeIncrementer(forIncrement: 10)
let inc7 = makeIncrementer(forIncrement: 7)
print(inc10()) // 10
print(inc10()) // 20
print(inc7()) // 7
print(inc10()) // 30 (与 inc7 互不干扰)
闭包是引用类型------ 循环引用根源
把闭包赋值给 let
常量时,常量里存的是引用。因此两个变量指向同一份闭包代码+捕获的存储。
swift
let alsoInc10 = inc10
alsoInc10() // 40,与 inc10 共享同一 runningTotal
逃逸闭包 @escaping
当闭包在函数返回后才被执行,必须显式标记 @escaping
。
常见场景:
- 异步回调(网络、动画);
- 存储到外部变量(数组、字典)。
swift
nonisolated(unsafe) var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(_ completion: @escaping () -> Void) {
completionHandlers.append(completion) // 逃出函数作用域
}
自动闭包 @autoclosure
自动把表达式包成无参闭包,实现延迟求值。断言、日志、调试框架大量使用。
swift
// 自定义 assert 风格函数
func myAssert(_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = "") {
if !condition() {
print("断言失败: \(message())")
}
}
myAssert(2 > 3, "2 不大于 3") // 表达式被包成闭包,只有失败才求值
捕获列表 ------ 破解循环引用
当逃逸闭包会捕获 self,而 self 又持有该闭包时,形成强引用环。
用捕获列表 [weak self]
或 [unowned self]
解决。
swift
class MyViewController {
var count = 0
@MainActor
func delayedPrint() {
// 逃逸闭包,10 秒后执行
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
guard let self = self else { return }
print(self.count)
}
}
}
结构体/枚举的逃逸限制
值类型不允许共享可变状态。
在 mutating
方法里,不能把捕获了 self
的逃逸闭包存起来,否则编译器直接报错。
解决思路:
- 把需要的数据拷贝一份再捕获;
- 改写成类(引用类型)。
实战:用闭包搭一个"迷你 DSL"
利用尾随闭包 + 自动闭包,30 行代码实现一个链式动画框架:
swift
// 定义
func animate(
duration: TimeInterval,
_ animations: @escaping () -> Void,
completion: @escaping () -> Void = {}
) {
UIView.animate(withDuration: duration,
animations: animations,
completion: { _ in completion() })
}
// 使用
animate(duration: 0.3) {
view.alpha = 0
} completion: {
print("消失完成")
}
总结与扩展
-
语法简写顺序
完整 → 省类型 → 省 return → 占位符 → 运算符。
-
捕获是"隐形延长生命周期",但逃逸必须显式声明。
-
值类型想逃逸,先复制;类类型想逃逸,先考虑
[weak/unowned]
。 -
自动闭包是"懒人包",断言、日志、路由延迟求值神器。
-
真实项目常见坑
- 网络层把
@escaping
漏写,升级 Swift 直接编译失败; flatMap
/compactMap
里写$0
导致可读性变差,团队规范要求>2 个参数必须显式命名;
- 网络层把