闭包
问题
Swift 的闭包是什么?捕获列表如何工作?@escaping 和 [weak self] 分别解决什么问题?
答案
闭包基础
闭包是自包含的函数代码块,可以捕获和存储其所在上下文中的变量和常量:
// 完整语法
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
// 简化:类型推断 + 隐式返回 + 简写参数
let add = { $0 + $1 }
add(1, 2) // 3
尾随闭包(Trailing Closure)
// 函数最后一个参数是闭包时,可以放在括号外面
let sorted = [3, 1, 4, 1, 5].sorted { $0 < $1 }
// 多个尾随闭包(Swift 5.3+)
UIView.animate(withDuration: 0.3) {
view.alpha = 0
} completion: { finished in
view.removeFromSuperview()
}
值捕获
闭包默认通过引用捕获外部变量(可以修改原始变量):
func makeCounter() -> () -> Int {
var count = 0
return { count += 1; return count } // 捕获了 count 的引用
}
let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3 —— count 被持续修改
Swift 闭包捕获的是变量本身(引用),而非变量的值。如果需要捕获当前值的快照,使用捕获列表。
捕获列表(Capture List)
用 [...] 在闭包参数前声明捕获方式:
var x = 10
// 捕获列表:创建闭包时拷贝 x 的当前值
let closure = { [x] in
print(x) // 10(不会跟随 x 的变化)
}
x = 20
closure() // 10 —— 仍然是捕获时的值
// 无捕获列表:引用原始变量
let closure2 = {
print(x) // 20(跟随 x 的变化)
}
closure2() // 20
@escaping 逃逸闭包
闭包默认是非逃逸的(在函数返回前执行完毕)。如果闭包在函数返回之后才执行,需要 @escaping:
// 非逃逸:闭包在 performOp 返回前执行
func performOp(_ op: () -> Void) {
op() // 立即执行
}
// 逃逸:闭包被存储,稍后执行
func fetchData(completion: @escaping (Data) -> Void) {
DispatchQueue.global().async {
let data = Data()
completion(data) // 函数已经返回,闭包仍在执行
}
}
| 非逃逸闭包 | 逃逸闭包 | |
|---|---|---|
| 生命周期 | 函数内执行完毕 | 函数返回后仍可执行 |
| 性能 | 编译器优化(栈分配) | 堆分配(需要延长生命周期) |
| self 引用 | 隐式使用 self | 必须显式写 self. |
| 常见场景 | map、filter、sorted | 网络回调、GCD、动画 completion |
[weak self] 与 [unowned self]
在逃逸闭包中引用 self 时,可能导致循环引用(retain cycle):
class ViewController: UIViewController {
var name = "Home"
func loadData() {
// ❌ 循环引用:闭包强引用 self,self 持有闭包
NetworkService.fetch { data in
self.name = data.title
}
// ✅ weak self:闭包不会阻止 self 被释放
NetworkService.fetch { [weak self] data in
guard let self else { return }
self.name = data.title
}
// ✅ unowned self:确定 self 一定存活时使用
NetworkService.fetch { [unowned self] data in
self.name = data.title // 如果 self 已释放会崩溃!
}
}
}
[weak self] | [unowned self] | |
|---|---|---|
| self 类型 | Self?(可选) | Self(非可选) |
| self 已释放 | 值为 nil,安全 | 崩溃(野指针) |
| 使用场景 | 不确定 self 是否存活 | 确定 self 一定存活(如 lazy 属性) |
| 推荐 | ✅ 大多数情况用这个 | ⚠️ 仅在明确生命周期时使用 |
@autoclosure 自动闭包
将表达式自动包装为闭包,延迟求值:
// 参数类型是 () -> Bool,但调用时可以直接传表达式
func assert(_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String) {
if !condition() {
print(message()) // 仅在条件不满足时才求值 message
}
}
// 调用时不需要写 {}
assert(1 > 0, "Math is broken") // condition 和 message 自动包装为闭包
闭包类型搭配高阶函数
let numbers = [1, 2, 3, 4, 5]
// map:转换
let doubled = numbers.map { $0 * 2 } // [2, 4, 6, 8, 10]
// filter:过滤
let evens = numbers.filter { $0 % 2 == 0 } // [2, 4]
// reduce:聚合
let sum = numbers.reduce(0) { $0 + $1 } // 15
// 等价于
let sum2 = numbers.reduce(0, +) // 15
// compactMap:转换 + 过滤 nil
let strings = ["1", "two", "3"]
let ints = strings.compactMap { Int($0) } // [1, 3]
// flatMap:展平
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 } // [1, 2, 3, 4]
// 链式调用
let result = numbers
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
.reduce(0, +) // 4 + 16 = 20
常见面试问题
Q1: Swift 闭包的捕获机制是怎样的?
答案:
Swift 闭包默认通过引用捕获外部变量——闭包持有变量的引用,可以读写原始变量。如果需要在闭包创建时拷贝变量的当前值(值捕获),使用捕获列表 [x]。对于引用类型,[weak x] 和 [unowned x] 控制引用强度。
Q2: @escaping 闭包为什么必须显式写 self?
答案:
逃逸闭包可能在函数返回后执行,可能导致循环引用。Swift 要求显式写 self 作为提醒——开发者需要意识到闭包正在捕获 self,从而决定是否使用 [weak self] 打破循环引用。非逃逸闭包在函数返回前执行完毕,不存在循环引用风险,所以可以隐式使用 self。
Q3: 值类型(struct)的闭包捕获有什么特殊之处?
答案:
struct 被闭包捕获时,闭包实际持有的是 struct 的拷贝。在 mutating 方法中使用闭包时需要注意:逃逸闭包不能捕获 mutating 的 self(因为逃逸后 struct 可能已经不在原始栈帧上了),这是编译器强制的安全检查。
Q4: 什么情况下闭包会导致循环引用?如何解决?
答案:
当对象 A 强引用闭包,闭包又强引用 A(通过 self)时,形成循环引用。常见于:
- VC 持有网络回调闭包,闭包中使用
self. - 对象持有定时器闭包
解决:在闭包捕获列表中使用 [weak self],在闭包内 guard let self else { return } 安全解包。
Q5: map、flatMap、compactMap 的区别?
答案:
map { f }→ 对每个元素应用 f,返回变换后的数组[U]flatMap { f }→ f 返回序列(如嵌套数组),展平一层[[U]] → [U]compactMap { f }→ f 返回 Optional,过滤掉 nil[U?] → [U]
注意:flatMap 在 Swift 4.1 起对 Optional 的展平功能已弃用,改用 compactMap。