首页 / Swift 编程语言教程 / 值捕获与捕获列表

Swift 编程语言教程

值捕获与捕获列表

本教程共 93 篇 · 第 36 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

Swift闭包值捕获捕获列表capture listweakunowned循环引用

本节目标:理解闭包”记住”外部变量的机制——值捕获,以及用捕获列表控制捕获方式、避免内存循环引用。

前面我们一直把闭包当成一个临时的小函数用,但闭包有一个函数做不到的本领:它能把定义时所在环境里的常量和变量”抓”进自己身体里,即使那个环境早就不存在了,闭包还能继续用这些变量。这就是值捕获(capturing values)。

1-1 什么是值捕获

简单说,闭包可以捕获并持有它定义时上下文中的常量和变量。被捕获之后,闭包体内部就能引用、甚至修改这些变量的值,哪怕定义它们的那个作用域早就已经结束了。Swift 帮你把所有捕获相关的内存管理都打理好,你不用手动释放。

最容易理解的捕获例子是嵌套函数。嵌套函数定义在另一个函数内部,它能捕获外层函数的参数和局部变量:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer 返回一个 incrementer 函数。注意,incrementer 体内并没有声明 runningTotalamount,它却直接用了。它靠的是从外层 makeIncrementer 捕获了这两个值。关键在于:捕获的是”引用”。这样即使 makeIncrementer 的函数调用结束了,runningTotal 也不会消失,下次再调用 incrementer 时它还在。

1-2 捕获让闭包有了”记忆”

光看代码可能没感觉,跑起来才明白。用刚才的函数造一个每次加 10 的闭包:

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()  // 返回 10
incrementByTen()  // 返回 20
incrementByTen()  // 返回 30

每调用一次,返回的数就比上次多 10。这说明 runningTotal 被闭包牢牢记住了,它不是每次都从 0 开始。你再造一个加 7 的闭包,它是独立的一份记忆:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()  // 返回 7
incrementByTen()    // 返回 40,不受 incrementBySeven 影响

两个闭包各自持有自己的 runningTotal,互不相干。这正是值捕获的威力:闭包把状态随身带着走。

Note

作为优化,如果某个值不会被闭包修改、且闭包创建后它也不变,Swift 可能改为捕获并保存这个值的一份拷贝,而不是引用。这是编译器的事,对你写代码没有影响。

1-3 闭包是引用类型带来的坑

上一章提过闭包是引用类型。把闭包赋给两个常量,它们指向同一个闭包,共享同一份捕获状态:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()  // 返回 50
incrementByTen()      // 返回 60

这里 alsoIncrementByTenincrementByTen 是同一个闭包的两个”把手”,调谁效果都一样。这个性质在配合类实例时要特别小心,稍不留神就会让类和闭包互相抓住不放,造成循环引用(retain cycle)。捕获取列表就是为此而生的。

1-4 捕获列表长什么样

捕获列表写在大括号开头、参数列表之前,用方括号包起来,里面列出你想以特殊方式捕获的东西。基本语法:

{ [捕获项1, 捕获项2] (参数) -> 返回类型 in
    语句
}

最常见的捕获项是 self,以及带修饰符的 [weak self][unowned self]。在逃逸闭包里引用类实例的 self 时,闭包会隐式强引用 self,很容易和实例形成循环。捕获列表的作用,就是改变这种引用的强度。

1-5 用 [weak] 和 [unowned] 打破循环

当闭包被赋值给某个类实例的属性,而闭包内部又用到了这个实例(比如引用 self.x),闭包和实例就互相持有,谁都释放不了。解决办法是在捕获列表里把 self 标成弱引用或无可引用:

class SomeClass {
    var x = 10
    var completion: (() -> Void)?
    func doSomething() {
        completion = { [self] in x = 100 }
    }
}

这里 [self] 仍然是强引用,只是显式写出来提醒自己”我捕获了 self”。真正要打破循环,得用 weakunowned

  • [weak self]:捕获的是可选类型的弱引用。闭包内 self 可能已经是 nil,所以用时通常需要解包。
  • [unowned self]:捕获的是无主引用,假定 self 一定还活着,访问时不会变成 nil,但一旦 self 真被释放了还去访问,会崩。

经验法则:如果闭包和实例生命周期不确定、可能互相先走一个,用 weak;如果你能确定闭包一定比实例活得短(比如闭包只在实例存活期间被调用),可以用 unowned。拿不准时,weak 更安全。

1-6 捕获列表还能指定值捕获

除了 weak/unowned,你也可以在捕获列表里写普通的名字,比如 [x],这表示按值捕获当前时刻的 x 拷贝,而不是引用。这在你想”冻结”某个变量、避免后续修改影响闭包时很有用。捕获列表里的每一项可以是以下形式之一:

  • 单纯名字 x,按引用或值取决于是否被修改;
  • 弱引用 [weak x]
  • 无主引用 [unowned x]
  • 无主且安全 [unowned(safe) x] 或 不安全 [unowned(unsafe) x]
Warning

weak 捕获进来的 self 是可选值,忘了解包直接访问 self!.xself?.x 是初学者最常踩的坑。用 guard let self = self else { return } 在闭包开头固定一份强引用,是最常见的稳妥写法。

1-7 捕获列表的细节与顺序

一个容易混淆的问题是:捕获列表里的项,到底是按值还是按引用捕获?规则是:如果你写单纯的名字(如 [x]),而 x 在闭包里会被修改,Swift 捕获的是引用;如果 x 不会被修改、且闭包创建后也不再变,Swift 可能改成捕获值的拷贝以作优化。写上 [weak x][unowned x] 时,捕获的当然是那个被弱化了的引用。理解这一点,你就不会奇怪”为什么有时候闭包里看到的外面变量是最新值,有时候又像被冻结了”。

另外要强调:捕获列表和闭包参数列表是两回事,别混在一起。捕获列表在最前面、用方括号;参数列表在捕获列表之后、用圆括号。完整顺序是 { [捕获列表] (参数) -> 返回 in 语句 }。初学者把 [weak self] 写在圆括号里是常见笔误,记住它在最外层的花括号一开头。

最后提醒:值捕获带来的循环引用,不只出现在逃逸闭包里,只要闭包被赋值给类实例的属性、且闭包内部又用到了那个实例,就可能形成环。捕获列表是解决它的标准手段,养成在闭包里引用 self 时先想一句”这里会不会成环”的习惯,能省掉无数后期的调试时间。

1-8 小结

值捕获让闭包能”记住”定义环境的变量,这正是它比普通函数灵活的地方。但引用捕获也带来循环引用隐患,捕获列表 [weak self][unowned self] 就是用来给这种强引用松绑的。关于逃逸闭包为何特别容易踩这个坑,下一章细说。