Swift 闭包(Closure)从入门到深入:语法、捕获与实战

前言

闭包是 Swift 的"灵魂语法"之一。它同时承担了

  1. 函数式编程的高阶函数;
  2. 面向对象中的委托回调;
  3. 异步并发中的逃逸闭包;
  4. 甚至属性包装器与 DSL 的构建基础。

闭包到底是什么?------ 一句话定义

闭包是自包含的代码块,可以在代码里被传递、被调用,同时自动捕获其定义时所在上下文中的常量和变量。

Swift 的闭包有三种形态:

  1. 全局函数:有名字,不捕获任何值。
  2. 嵌套函数:有名字,可捕获外层函数局部量。
  3. 闭包表达式:无名字,轻量级语法,可捕获上下文。

闭包表达式语法拆解

最简形式:

swift 复制代码
{ (parameters) -> returnType in
    statements
}

下面用"反向排序"一例,把 5 次迭代全部还原

swift 复制代码
// 0. 原始数组
let names = ["Chris", "Alex", "Ewa", "Barry", "Dani"]

// 1. 最原始:写一个普通函数
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2   // 按字母降序
}
let reversed1 = names.sorted(by: backward)
print(reversed1)   // ["Ewa", "Dani", "Chris", "Barry", "Alex"]

// 2. 第一次简化:写成完整闭包表达式
let reversed2 = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})
print(reversed2)

// 3. 第二次简化:类型推断,省略参数/返回类型
let reversed3 = names.sorted(by: { s1, s2 in return s1 > s2 })
print(reversed3)

// 4. 第三次简化:单表达式可省 return
let reversed4 = names.sorted(by: { s1, s2 in s1 > s2 })
print(reversed4)

// 5. 第四次简化:使用 $0、$1 占位符
let reversed5 = names.sorted(by: { $0 > $1 })
print(reversed5)

// 6. 第五次简化:直接传运算符
let reversed6 = names.sorted(by: >)
print(reversed6)

Trailing Closure ------ 尾随闭包

当闭包是最后一个参数且较长时,可写在调用括号外,增强可读性。

单参数可省括号;多参数时,第一个尾随闭包可省标签,其余必须带标签。

swift 复制代码
// 0. 原始数组
let names = ["Chris", "Alex", "Ewa", "Barry", "Dani"]

// 单参数,括号直接省
let reversed7 = names.sorted { $0 > $1 }

class Server {}
class Picture {}
let server = Server()
func show(_ picture: Picture) {
    print("一张图片")
}
func show(_ e: Error) {
    print("一个错误")
}

// 多参数:loadPicture
func loadPicture(
    from server: Server,
    completion: @escaping (Picture) -> Void,
    onFailure: @escaping (Error) -> Void
) { /* 网络代码 */ }

loadPicture(from: server) { picture in
    show(picture)
} onFailure: { error in
    show(error)
}

捕获值(Capturing Values)------ 闭包的"黑魔法"

嵌套函数/闭包表达式可以延长局部变量的生命周期。

swift 复制代码
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0          // 外层局部变量
    func incrementer() -> Int {   // 嵌套函数
        runningTotal += amount    // 捕获两个量
        return runningTotal
    }
    return incrementer
}

let inc10 = makeIncrementer(forIncrement: 10)
let inc7  = makeIncrementer(forIncrement: 7)

print(inc10()) // 10
print(inc10()) // 20
print(inc7())  // 7
print(inc10()) // 30 (与 inc7 互不干扰)

闭包是引用类型------ 循环引用根源

把闭包赋值给 let 常量时,常量里存的是引用。因此两个变量指向同一份闭包代码+捕获的存储。

swift 复制代码
let alsoInc10 = inc10
alsoInc10() // 40,与 inc10 共享同一 runningTotal

逃逸闭包 @escaping

当闭包在函数返回后才被执行,必须显式标记 @escaping

常见场景:

  1. 异步回调(网络、动画);
  2. 存储到外部变量(数组、字典)。
swift 复制代码
nonisolated(unsafe) var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(_ completion: @escaping () -> Void) {
    completionHandlers.append(completion) // 逃出函数作用域
}

自动闭包 @autoclosure

自动把表达式包成无参闭包,实现延迟求值。断言、日志、调试框架大量使用。

swift 复制代码
// 自定义 assert 风格函数
func myAssert(_ condition: @autoclosure () -> Bool,
              _ message: @autoclosure () -> String = "") {
    if !condition() {
        print("断言失败: \(message())")
    }
}

myAssert(2 > 3, "2 不大于 3")   // 表达式被包成闭包,只有失败才求值

捕获列表 ------ 破解循环引用

当逃逸闭包会捕获 self,而 self 又持有该闭包时,形成强引用环。

用捕获列表 [weak self][unowned self] 解决。

swift 复制代码
class MyViewController {
    var count = 0
    
    @MainActor
    func delayedPrint() {
        // 逃逸闭包,10 秒后执行
        DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
            guard let self = self else { return }
            print(self.count)
        }
    }
}

结构体/枚举的逃逸限制

值类型不允许共享可变状态。

mutating 方法里,不能把捕获了 self 的逃逸闭包存起来,否则编译器直接报错。

解决思路:

  1. 把需要的数据拷贝一份再捕获;
  2. 改写成类(引用类型)。

实战:用闭包搭一个"迷你 DSL"

利用尾随闭包 + 自动闭包,30 行代码实现一个链式动画框架:

swift 复制代码
// 定义
func animate(
    duration: TimeInterval,
    _ animations: @escaping () -> Void,
    completion: @escaping () -> Void = {}
) {
    UIView.animate(withDuration: duration,
                   animations: animations,
                   completion: { _ in completion() })
}

// 使用
animate(duration: 0.3) {
    view.alpha = 0
} completion: {
    print("消失完成")
}

总结与扩展

  1. 语法简写顺序

    完整 → 省类型 → 省 return → 占位符 → 运算符。

  2. 捕获是"隐形延长生命周期",但逃逸必须显式声明。

  3. 值类型想逃逸,先复制;类类型想逃逸,先考虑 [weak/unowned]

  4. 自动闭包是"懒人包",断言、日志、路由延迟求值神器。

  5. 真实项目常见坑

    • 网络层把 @escaping 漏写,升级 Swift 直接编译失败;
    • flatMap/compactMap 里写 $0 导致可读性变差,团队规范要求>2 个参数必须显式命名;
相关推荐
HarderCoder11 小时前
Swift 集合类型详解(三):自定义集合、持久化结构与 ORM 共舞
swift
HarderCoder11 小时前
Swift 集合类型详解(一):Array、Set、Dictionary 全貌与选型思路
swift
HarderCoder11 小时前
Swift 集合类型详解(二):自定义 Hashable、值语义与性能陷阱
swift
东坡肘子16 小时前
Sora 2:好模型,但未必是好生意 | 肘子的 Swift 周报 #0105
人工智能·swiftui·swift
HarderCoder1 天前
Swift 6 并发深渊:@unchecked Sendable 与“隐式 MainActor”如何合谋杀死你的 App
swiftui·swift
HarderCoder1 天前
告别 UIKit 生命周期:SwiftUI 视图一生全解析——从 init 到 deinit 的“隐秘角落”
swiftui·swift
HarderCoder1 天前
Swift 中的基本运算符:从加减乘除到逻辑与或非
ios·swift
HarderCoder1 天前
Swift 中“特性开关”实战笔记——用编译条件+EnvironmentValues优雅管理Debug/TestFlight/AppStore三环境
ios·swift
HarderCoder1 天前
Swift 并发任务中到底该不该用 `[weak self]`?—— 从原理到实战一次讲透
ios·swift