Swift 闭包
闭包是一段可以像值一样传递和使用的代码块,它能捕获并保存定义处周围的变量。
在 Swift 里,全局函数、嵌套函数和闭包表达式本质上都是闭包,区别只在于写法与命名。
本文讲解闭包表达式的简化过程、尾随闭包、捕获机制,以及逃逸与非逃逸这两个容易出错的概念。
闭包的三种形式
Swift 官方把闭包分成三种形式,它们的能力一致,只是形态不同。
| 形式 | 写法 | 特点 |
|---|---|---|
| 全局函数 | func name(...) { } | 有名字,不捕获任何值 |
| 嵌套函数 | 定义在另一个函数内部 | 有名字,可以捕获外层函数的变量 |
| 闭包表达式 | { ... } | 没有名字,用轻量语法书写,可捕获上下文 |
闭包表达式语法
闭包表达式用一对大括号包住,参数和返回类型写在 in 前面,代码写在 in 后面。
完整形式是 { (参数) -> 返回类型 in 语句 }。
实例
// 完整写法:参数列表、返回类型、in 关键字缺一不可
let full = { (a: Int, b: Int) -> Int in
return a + b
}
print(full(3, 4))
以上代码执行输出结果为:
7
依靠类型推断简化
当闭包的类型已经确定时,参数类型和返回类型都可以省略。
实例
// 有了左边的类型标注,右边可以省略参数类型和返回类型
let inferred: (Int, Int) -> Int = { a, b in a * b }
print(inferred(3, 4))
以上代码执行输出结果为:
12
用 $0、$1 简写参数
参数名也可以省掉,用 $0 表示第一个参数,$1 表示第二个,依次类推。
闭包体只有一行时,连 return 都可以省略。
实例
// $0 表示第一个参数,$1 表示第二个参数
let shorthand: (Int, Int) -> Int = { $0 - $1 }
print(shorthand(10, 4))
以上代码执行输出结果为:
6
注意:简写虽然短,但参数一多就难读。超过两个参数时,建议写回具名参数。
运算符函数
Swift 的运算符本质上就是函数,所以可以直接把 < 这样的运算符当成闭包传入。
实例
let numbers = [3, 1, 4, 1, 5, 9, 2, 6]
// 直接把 < 运算符当作闭包传入
let sorted = numbers.sorted(by: <)
print(sorted)
以上代码执行输出结果为:
[1, 1, 2, 3, 4, 5, 6, 9]
尾随闭包
当闭包是函数的最后一个参数时,可以把它写在函数调用的括号外面,这种写法叫尾随闭包。
尾随闭包能省掉一层括号,让代码更接近「配置块」的阅读体验。
实例
let numbers = [3, 1, 4, 1, 5, 9, 2, 6]
// 尾随闭包:闭包写在函数调用的括号外面
let evens = numbers.filter { $0 % 2 == 0 }
print(evens)
以上代码执行输出结果为:
[4, 2, 6]
多尾随闭包
函数有多个闭包参数时,第一个可以省略标签,后面的必须写上标签。
这种写法常见于带「成功 / 失败」两个回调的接口。
实例
// 两个闭包参数,第一个用尾随闭包,第二个用带标签的尾随闭包
func load(from source: String, onSuccess: () -> Void, onFailure: () -> Void) {
if source.hasPrefix("https") {
onSuccess()
} else {
onFailure()
}
}
load(from: "https://www.runoob.com") {
print("加载成功")
} onFailure: {
print("加载失败")
}
以上代码执行输出结果为:
加载成功
捕获值
闭包能捕获定义处外部的变量和常量,即使定义它们的函数已经返回,这些值依然存在。
下面的 count 被返回的闭包捕获,每次调用都会在原来的基础上加一。
实例
// 返回的闭包捕获了外部的 count 变量
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter())
print(counter())
print(counter())
以上代码执行输出结果为:
1 2 3
闭包是引用类型
把闭包赋给另一个变量时,不会复制捕获的值,两个变量指向同一个闭包和同一份状态。
实例
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
// 闭包是引用类型,counterA 与 counterB 指向同一个闭包
let counterA = makeCounter()
let counterB = counterA
print(counterA())
print(counterB())
以上代码执行输出结果为:
1 2
第二次打印出 2,说明两次调用共享同一个 count。
捕获列表 [weak self] 与 [unowned self]
在类中把闭包存进属性时,闭包会强引用 self,容易形成循环引用。
捕获列表写在参数列表之前,用来指定捕获方式,从而打破这个环。
[weak self]
weak 是弱引用,对象释放后自动变成 nil,因此访问时要先解包。
实例
class Downloader {
var site = "RUNOOB"
var onFinish: (() -> Void)?
func start() {
// [weak self] 让闭包弱引用 self,避免循环引用
onFinish = { [weak self] in
guard let self else { return }
print("下载完成:\(self.site)")
}
}
deinit {
print("Downloader 被释放")
}
}
var downloader: Downloader? = Downloader()
downloader?.start()
downloader?.onFinish?()
downloader = nil
以上代码执行输出结果为:
下载完成:RUNOOB Downloader 被释放
因为用的是弱引用,把 downloader 置为 nil 后对象能被正常释放。
[unowned self]
unowned 也是不增加引用计数,但它假设对象一定还活着,访问时不需要解包。
一旦对象已经释放却仍然访问,程序会直接崩溃,所以它只适合生命周期确定不会错开的关系。
实例
class TimerOwner {
var seconds = 0
// [unowned self] 不增加引用计数,也不产生可选值
lazy var tick: () -> Int = { [unowned self] in
self.seconds += 1
return self.seconds
}
}
let owner = TimerOwner()
print(owner.tick())
print(owner.tick())
以上代码执行输出结果为:
1 2
注意:拿不准用哪个时,选 [weak self] 并配合 guard let self else { return }。它更安全,代价只是一次可选值解包。
逃逸闭包与非逃逸闭包
函数类型参数默认是非逃逸的:闭包保证在函数返回前执行完。
需要保存下来、在函数返回之后才调用的闭包,必须标注 @escaping。
| 对比项 | 非逃逸闭包 | 逃逸闭包 |
|---|---|---|
| 是否需要标注 | 不用 | 需要 @escaping |
| 生命周期 | 函数返回前结束 | 可以活过函数调用 |
| 能否存入属性或数组 | 不能 | 可以 |
| 引用 self 时的要求 | 直接写即可 | 类中通常要显式写 self,并考虑捕获列表 |
实例
// 非逃逸:闭包在函数返回前就执行完
func perform(_ action: () -> Void) {
action()
}
perform {
print("非逃逸闭包立即执行")
}
// 逃逸:闭包被保存到属性,函数返回后才调用
class Store {
var stored: (() -> Void)?
func save(_ action: @escaping () -> Void) {
stored = action
}
}
let store = Store()
store.save { print("逃逸闭包稍后执行") }
store.stored?()
以上代码执行输出结果为:
非逃逸闭包立即执行 逃逸闭包稍后执行
常见问题
下面是闭包语法与捕获机制中常见的几个疑问。
闭包和函数有什么区别
本质相同。函数是有名字的闭包,闭包表达式是没有名字、写法更轻量的闭包。
两者都能捕获上下文,也都能作为参数和返回值传递。
什么时候必须写 self
在类或结构体的实例方法里写逃逸闭包时,Swift 要求显式写出 self,提醒你可能产生循环引用。
非逃逸闭包里通常可以省略,写不写都能编译。
尾随闭包能嵌套吗
可以,但嵌套过深会难以阅读,建议把内层闭包提取成具名函数或单独的变量。
为什么闭包里的变量能一直活着
闭包捕获变量时会持有它,只要闭包本身还被引用,捕获的变量就不会被销毁。
这也是闭包可能造成内存泄漏的根源,详见第 33 篇《Swift 自动引用计数》。
