Swift 闭包
闭包(closure)是 Swift 里"自包含的代码块",本质和你写过的函数一模一样——只是没有名字,可以被当作值传来传去。它和协议一起,构成 Swift 抽象能力的"双子星"。SwiftUI 的视图、网络请求的回调、数组的 map/filter,几乎处处是闭包。本章把它讲透。
1. 闭包的基本语法
闭包完整语法以 in 关键字分隔:in 前是参数列表与返回类型,in 后是函数体(完整形式见下方代码示例)。和函数最大的不同是:闭包没有 func 关键字、没有函数名,但有参数列表、返回类型和函数体:
// 闭包:一段"可以被传递的代码块"(无名函数/lambda)
// 完整语法:{ (参数) -> 返回类型 in 代码 }
// 1. 用变量存一个闭包
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
print(add(3, 4)) // 7
// 2. 闭包作参数(最常见用法)
func operate(_ x: Int, _ y: Int, op: (Int, Int) -> Int) -> Int {
return op(x, y)
}
print(operate(10, 3, op: add)) // 13
// 3. 直接内联写闭包
print(operate(10, 3, op: { (a, b) in a * b })) // 30关键点:
- 闭包是一等公民:可以存进变量、当参数传、当返回值返。
- 类型签名:
(Int, Int) -> Int表示"接受两个 Int、返回 Int 的闭包"。 - 无返回值可写 Void:等价于
() -> Void。
2. 尾随闭包(Swift 的招牌语法)
当函数最后一个参数是闭包时,Swift 提供一种优雅的简写——把闭包提到括号外,省掉标签。这就是 SwiftUI 简洁可读的根源:
// 尾随闭包:当最后一个参数是闭包时,可以把它提到括号外
func loadConfig(_ key: String, builder: () -> String) -> String {
return "\(key)=\(builder())"
}
// 普通写法
let v1 = loadConfig("host", builder: { return "localhost" })
// 尾随闭包(省略 builder 标签,提到括号外)
let v2 = loadConfig("host") {
return "localhost"
}
// 多个尾随闭包(Swift 5.3+):第一个可省标签,后续保留标签
func fetchData(success: () -> Void, failure: () -> Void) {}
fetchData {
print("成功")
} failure: {
print("失败")
}
// SwiftUI 大量用尾随闭包,如 Button(action:) { Text("点我") }这条规则让 Swift 写 UI 时几乎像声明式语言。比如 SwiftUI 里 Button、VStack 这些视图全都靠尾随闭包构建内容。
3. $0 简写:省到极致
Swift 还允许你完全不写参数名,用 $0、$1、$2 自动引用第几个参数。配合尾随闭包,闭包可以写得极短:
// 参数简写:$0、$1、$2... 自动代表第几个参数
let multiply: (Int, Int) -> Int = { $0 * $1 }
print(multiply(6, 7)) // 42
// 排序:闭包返回 true 表示前者应在前
let nums = [3, 1, 4, 1, 5, 9, 2, 6]
let asc = nums.sorted(by: { $0 < $1 }) // 升序
let desc = nums.sorted { $0 > $1 } // 降序 + 尾随闭包
print(asc) // [1, 1, 2, 3, 4, 5, 6, 9]
print(desc) // [9, 6, 5, 4, 3, 2, 1, 1]
// map / filter / reduce 是闭包的高频舞台
let doubled = nums.map { $0 * 2 } // [6, 2, 8, 2, 10, 18, 4, 12]
let evens = nums.filter { $0 % 2 == 0 } // [4, 2, 6]
let sum = nums.reduce(0) { $0 + $1 } // 31这一组(map/filter/reduce + 简写)是日常 Swift 开发里闭包出场率最高的组合拳,务必熟练。
4. 值捕获
闭包会捕获它引用的外层变量,让这些变量在原作用域销毁后仍然存活。这是闭包区别于普通代码块的核心特性:
// 闭包会"捕获"它引用的外层变量,让变量在闭包里继续存活
func makeCounter() -> () -> Int {
var count = 0 // 局部变量
return {
count += 1 // 捕获 count
return count
}
}
let counter = makeCounter()
print(counter()) // 1
print(counter()) // 2
print(counter()) // 3
// 注意:count 是 makeCounter 的局部变量,本应出栈销毁
// 但闭包"持有"了它,所以能跨调用累加——这就是捕获注意 Swift 默认是引用捕获(修改会影响外层),如果想要快照,用捕获列表——在闭包开头的方括号里列出要捕获的变量,会把当前值复制一份。
5. 逃逸闭包:@escaping
默认情况下,Swift 闭包是非逃逸(@noescape)的——必须在函数返回前用完。如果闭包需要在函数返回后才执行(如异步网络请求、定时器、动画完成回调),必须显式标 @escaping:
// 逃逸闭包:@escaping
// "逃逸"= 闭包在函数返回之后才被调用(如异步、定时器、网络请求)
var completed: () -> Void = {}
// 默认是非逃逸(@noescape):闭包必须在函数返回前用完
func runNow(_ block: () -> Void) {
block() // 同步调用,安全
}
// 逃逸:闭包被存到外部,函数返回后才执行 → 必须加 @escaping
func runLater(_ block: @escaping () -> Void) {
completed = block // 存到外部变量
}
runNow { print("立即") }
runLater { print("稍后") }
completed() // 现在才真正调用 → "稍后"
// 网络请求、UIView 动画、DispatchQueue.async 的回调都是 @escaping
// DispatchQueue.global().async { ... } 的闭包就是逃逸的为什么要区分?非逃逸闭包编译器可以优化,且不需要担心 self 捕获造成的循环引用(因为函数返回时闭包就销毁了)。一旦标 @escaping,编译器就要求你显式处理 self。
6. 循环引用与 weak self
闭包捕获 self 时,如果 self 又持有这个闭包,就会形成循环引用——内存永远释放不了。这是 Swift 内存管理最常见的坑:
// 循环引用:闭包捕获 self 会造成内存泄漏
class NetworkManager {
var name = "default"
var onDone: (() -> Void)?
func fetch() {
// ❌ 强引用 self → self 强引用 onDone → onDone 强引用 self → 泄漏
onDone = {
print(self.name) // 捕获 self
}
}
}
// ✅ 解决:用捕获列表 [weak self] 或 [unowned self]
class SafeManager {
var name = "default"
var onDone: (() -> Void)?
func fetch() {
onDone = { [weak self] in // weak → self 变成可选
guard let self = self else { return }
print(self.name)
}
}
deinit { print("SafeManager 释放") }
}
// weak:引用对象释放后自动变 nil(可选)
// unowned:引用对象释放后再访问会崩溃(非可选,确信生命周期一致时用)两条规则:
- 非逃逸闭包:不会循环引用,不需要 weak。
- 逃逸闭包且捕获 self:需要
[weak self](变可选)或[unowned self](非可选,确信不会变 nil)。
实战中绝大多数用 [weak self],unowned 留给"双方必定同时销毁"的紧密关系。
7. 闭包 vs 函数
| 特性 | 函数 func | 闭包 { } |
|---|---|---|
| 有名字 | ✓ | ✗(可赋给变量获得名字) |
| 能当值传递 | ✓ | ✓ |
| 捕获外层变量 | ✗ 嵌套函数也能 | ✓ |
| 尾随语法 | ✗ | ✓ |
| $0 简写 | ✗ | ✓ |
函数本质是"有名字的闭包"。日常:可复用逻辑用函数,一次性内联逻辑用闭包。
常见陷阱
- 循环引用:逃逸闭包忘记
[weak self],对象永不释放。开着 Xcode Memory Graph 一眼能看出来。 - 误把非逃逸当逃逸:同步调用的闭包不需要
@escaping,加了反而要写 self,啰嗦。 - 过度使用简写:三层嵌套全
$0,可读性极差。命名参数(写出user、age等具体名字)并不丢人。 - 多尾随闭包:Swift 5.3+ 才支持,旧代码升级注意。
小结
Swift 闭包 = 无名函数 + 尾随闭包语法 + $0 简写 + 值捕获 + @escaping + [weak self]。掌握它,你就能读懂 SwiftUI 一半以上的代码,也能写出干净的异步逻辑。下一篇讲错误处理——try/throw/catch,以及 Result 类型如何把"可能失败的闭包"做得更优雅。
← 上一篇 Swift 协议
下一篇 Swift 错误处理 →