defer — Swift 中的一种控制结构,它将代码块的执行安排在退出当前作用域(scope)的时刻。defer 块的执行与结束方式无关 — return、break、throw、fatalError 或正常结束。根据 Swift Language Guide(2025),当同一个作用域中存在多个 defer 时,它们按声明的相反顺序执行 — 最后声明的首先执行(LIFO)。这使得 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 时,它们按声明的相反顺序执行(LIFO — Last In, First Out)。这意味着最后声明的 defer 将首先执行,而第一个则最后执行:
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 的主要用途 — 保证资源清理。让我们考虑文件系统操作。通过 FileHandle 打开文件需要显式关闭 — defer 保证在任何场景下都会调用 close:
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 与 throws 函数有效结合。当函数可能在任意阶段抛出错误时,defer 保证清理而无需在每个 catch 块或 guard 提前退出中重复代码:
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 循环内部 — 则在每次迭代时执行:
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 声明后发生的变化将影响其执行。
第一个错误 — 假设执行顺序不同于 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 内部处理错误。
常见问题
defer — 一种 Swift 控制结构,将块的执行延迟到退出当前作用域时。块总是在 return、throw、break 或正常结束时执行。用于保证资源清理:关闭文件、解除锁。
按声明的相反顺序(LIFO) — 最后声明的 defer 首先执行。这保证了嵌套资源的正确清理:如果资源 B 在 A 之后打开,将在 A 之前关闭,从而防止对已释放资源的依赖。
不能直接抛出 — Swift 禁止错误从 defer 传播。对于可能抛出错误的操作,使用 try? 或 try!。最佳实践是将清理函数设为 non-throwing,或在 defer 内部处理错误而不向外传播。
defer 与作用域绑定,在任何退出时执行,包括 return、throw 和 break。finally(在其他语言中)与 try-catch 绑定,仅在存在 try 时执行。Swift 中没有 finally — defer 完全覆盖了这一场景,并且适用于任何作用域,而不仅仅是错误处理。
是的,defer 在退出作用域时读取变量,而非声明时。如果变量在 defer 声明后发生变化,defer 块将看到最后一个值。这与普通闭包不同,后者在创建时捕获被固定。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。