defer — 是什么、语法及执行顺序

作者: IT Sectr 发布日期: 2026-06-20 阅读时间: 8 分钟

defer — Swift 中的一种控制结构,它将代码块的执行安排在退出当前作用域(scope)的时刻。defer 块的执行与结束方式无关 — returnbreakthrowfatalError 或正常结束。根据 Swift Language Guide(2025),当同一个作用域中存在多个 defer 时,它们按声明的相反顺序执行 — 最后声明的首先执行(LIFO)。这使得 defer 成为保证资源清理不可或缺的工具:关闭文件描述符、解除锁、释放临时指针,而无需担心在提前退出时遗漏清理。

要点

  • defer — 在退出作用域时执行的代码块,与退出原因无关(return、throw、break)
  • LIFO 顺序 — 多个 defer 从下到上执行 — 最后声明的最先触发
  • 资源清理 — 主要用途:关闭文件、解除锁、结束动画
  • 变量定义:defer 在退出时看到变量,而不是在声明时
  • 不替代 do-catch 进行错误处理 — defer 负责清理,而非流程控制

什么是 defer?

defer — 是 Swift 语言中的一种控制结构,于 Swift 2.0(2015)引入,它将块的执行延迟到当前作用域结束的时刻。关键特性:defer 保证其主体的执行,无论作用域以何种方式结束 — 成功(return)、出错(throw)、提前退出(break、continue)或致命错误(fatalError、precondition)。

语法上,defer 看起来像 defer { /* 代码 */ },可以放置于作用域内的任何位置。Swift 编译器保证 defer 内的代码会被执行,即使在 defer 声明和作用域结束之间发生了异常或 return。这从根本上将 defer 与放置在函数末尾的普通代码区分开来,后者在提前退出时可能被跳过。

根据 Chris Lattner(Swift 创建者,2015)的文章,defer 受到其他语言类似结构的启发 — Go 的 defer、Java/Python 的 finally、C++ 的 scope guard — 但有一个重要区别:在 Swift 中,defer 在作用域结束时执行,而不是在 try-catch 块之后立即执行。这为具有多个退出点的函数的清理提供了更可预测的行为。

使用 defer 进行对称资源管理:打开文件 → defer { close },设置锁 → defer { unlock }。这样的模式保证在任何情况下都不会遗漏资源的释放。

多个 defer 的执行顺序

当同一个作用域中声明了多个 defer 时,它们按声明的相反顺序执行(LIFO — Last In, First Out)。这意味着最后声明的 defer 将首先执行,而第一个则最后执行:

swift
func exampleDeferOrder() {
    defer { print("第一个 defer") }
    defer { print("第二个 defer") }
    defer { print("第三个 defer") }

    print("函数体")
}
// 输出:
// 函数体
// 第三个 defer
// 第二个 defer
// 第一个 defer

LIFO 顺序对于正确管理嵌套资源非常重要。如果先打开文件 A,然后打开文件 B,则必须以相反顺序释放它们:先 B,后 A。使用 defer,这会自动发生 — 在打开每个资源后立即声明 defer,无论函数有多少个退出点,清理顺序都将正确。

根据 Swift by Sundell(2024),这一特性使 defer 成为嵌套锁和事务的理想选择:获取锁 → defer { unlock } → 获取下一个 → defer { unlock }。LIFO 保证锁以获取的相反顺序释放,防止死锁。

Defer 用于资源清理

defer 的主要用途 — 保证资源清理。让我们考虑文件系统操作。通过 FileHandle 打开文件需要显式关闭 — defer 保证在任何场景下都会调用 close:

swift
func readFile(path: String) throws -> String {
    let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
    defer { try? handle.close() }

    let data = try handle.readToEnd()
    guard let data else { throw FileError.empty() }

    return String(data: data, encoding: .utf8) ?? ""
    // handle.close() 即使在 throw 或 return 时也会被调用
}

另一个典型场景 — 带加载标志的 UI 动画。在开始加载之前,设置标志 isLoading = true,defer 在函数退出时将其改为 false,无论请求成功或出错。这可以防止因未处理的错误导致标志保持为 true 而永久阻塞界面。

根据 Bitbucket Engineering Blog(2024),defer 也用于性能分析:在函数开始时记录时间,在 defer 中计算并输出差值。这为所有执行路径(包括错误路径)提供了准确的性能测量。

Defer 和错误处理

deferthrows 函数有效结合。当函数可能在任意阶段抛出错误时,defer 保证清理而无需在每个 catch 块或 guard 提前退出中重复代码:

swift
func processTransaction() throws {
    let db = try openDatabase()
    defer { closeDatabase(db) }

    let user = try fetchUser(from: db)
    defer { logAudit(user) }

    let result = try performPayment(user)
    sendNotification(result)
    // closeDatabase(db) 和 logAudit(user) 将在每次 throw 或 return 时被调用
    // 在每次 throw 或 return 时
}

重要:defer 在控制权从 catch 块转移执行,但在错误发生之后。如果 defer 中抛出错误,Swift 不允许在 defer 内直接使用 try — 需要使用 try?try!。根据 Apple 文档,Swift 不允许错误从 defer 中「逃出」,因为这会破坏块的执行保证。

在获取资源后立即放置 defer。这遵循就近原则:读者看到获取和释放紧挨在一起,提高了代码的可靠性并简化了代码审查。

作用域规则

defer 在退出其声明所在的作用域时执行。如果 defer 声明在 do 块内部,则在退出该块时执行,而非外部函数。如果在 for 循环内部 — 则在每次迭代时执行:

swift
func scopeExample() {
    print("start")
    do {
        defer { print("do 块 defer") }
        print("inside do")
    }
    // "do 块 defer" 在此打印
    print("after do")
}
    // 输出:start, inside do, do 块 defer, after do

for i in 1...3 {
    defer { print("end iteration \(i)") }
    print("iteration \(i)")
}
    // 输出:迭代 1, 迭代 1 结束, 迭代 2, 迭代 2 结束, ...

defer 捕获的变量在退出作用域时被读取,而不是在 defer 声明时。如果变量在 defer 声明和作用域结束之间发生变化,defer 将看到最后一个值。这是与闭包(closures)的重要区别,后者在创建时捕获。请注意:变量在 defer 声明后发生的变化将影响其执行。

使用 defer 的常见错误

第一个错误 — 假设执行顺序不同于 LIFO。如果清理顺序很重要,而 defer 以错误的顺序声明,资源可能会违反依赖关系被释放。解决方案:在获取每个资源后立即声明 defer。第二个资源已打开 → defer { close second } 在第一个资源关闭之前。

第二个错误 — 将 defer 用于与清理无关的逻辑。defer 旨在保证清理,而非主要流程控制。如果 defer 中的代码影响返回值,这几乎总是错误。defer 不能更改函数的 return 值(与 Java 的 finally 不同,后者 finally 中的 return 会覆盖原始的 return)。

第三个错误 — 从 defer 抛出错误。Swift 禁止在 defer 内使用 try(如果错误可能传播到外部)。对于可能抛出错误的操作,使用 try?try!,或将其包装在单独的 throws-free 函数中。根据 O'Reilly《Swift 深入理解》(2025),好的做法是使清理函数为非抛出类型(non-throwing)或在 defer 内部处理错误。

常见问题

Swift 中的 defer 是什么?

defer — 一种 Swift 控制结构,将块的执行延迟到退出当前作用域时。块总是在 return、throw、break 或正常结束时执行。用于保证资源清理:关闭文件、解除锁。

多个 defer 按什么顺序执行?

按声明的相反顺序(LIFO) — 最后声明的 defer 首先执行。这保证了嵌套资源的正确清理:如果资源 B 在 A 之后打开,将在 A 之前关闭,从而防止对已释放资源的依赖。

可以从 defer 抛出错误吗?

不能直接抛出 — Swift 禁止错误从 defer 传播。对于可能抛出错误的操作,使用 try?try!。最佳实践是将清理函数设为 non-throwing,或在 defer 内部处理错误而不向外传播。

defer 和 do-catch-finally 有什么区别?

defer 与作用域绑定,在任何退出时执行,包括 return、throw 和 break。finally(在其他语言中)与 try-catch 绑定,仅在存在 try 时执行。Swift 中没有 finally — defer 完全覆盖了这一场景,并且适用于任何作用域,而不仅仅是错误处理。

defer 能看到其声明后变量的变化吗?

是的,defer 在退出作用域时读取变量,而非声明时。如果变量在 defer 声明后发生变化,defer 块将看到最后一个值。这与普通闭包不同,后者在创建时捕获被固定。

总结

  • defer — 在退出作用域时执行的终结块,与原因无关(return、throw、break、正常结束)
  • LIFO 顺序 — 多个 defer 从下到上执行,最后声明的 — 首先执行的
  • 资源清理 — 主要用途:关闭文件、解除锁、释放指针、停止动画
  • 与throws兼容:defer 在错误之后但在退出 catch 块之前执行;defer 中的错误不会传播
  • 作用域:defer 在退出其声明所在的作用域时执行 — do 块、循环、函数
  • 变量捕获:defer 在退出时读取值,而非声明时 — 注意变量突变
  • 最佳实践:在获取资源后立即声明 defer,不要将 defer 用于业务逻辑,使清理函数为非抛出类型

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读