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

关键点:

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 里 ButtonVStack 这些视图全都靠尾随闭包构建内容。

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],unowned 留给"双方必定同时销毁"的紧密关系。

7. 闭包 vs 函数

特性函数 func闭包 { }
有名字✗(可赋给变量获得名字)
能当值传递
捕获外层变量✗ 嵌套函数也能
尾随语法
$0 简写

函数本质是"有名字的闭包"。日常:可复用逻辑用函数,一次性内联逻辑用闭包

常见陷阱

小结

Swift 闭包 = 无名函数 + 尾随闭包语法 + $0 简写 + 值捕获 + @escaping + [weak self]。掌握它,你就能读懂 SwiftUI 一半以上的代码,也能写出干净的异步逻辑。下一篇讲错误处理——try/throw/catch,以及 Result 类型如何把"可能失败的闭包"做得更优雅。

← 上一篇 Swift 协议

下一篇 Swift 错误处理

✈️💬