跳到主要内容

闭包

问题

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.
常见场景mapfiltersorted网络回调、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 vs unowned
[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: mapflatMapcompactMap 的区别?

答案

  • map { f } → 对每个元素应用 f,返回变换后的数组 [U]
  • flatMap { f } → f 返回序列(如嵌套数组),展平一层 [[U]] → [U]
  • compactMap { f } → f 返回 Optional,过滤掉 nil [U?] → [U]

注意:flatMap 在 Swift 4.1 起对 Optional 的展平功能已弃用,改用 compactMap

相关链接