首页 / Swift 编程语言教程 / defer 延迟执行

Swift 编程语言教程

defer 延迟执行

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

SwiftSwift 编程语言教程defer延迟执行清理作用域退出LIFO错误处理

本节目标:掌握 defer 语句,让一段清理代码在作用域退出前必定执行,无论是因为正常结束、return 还是抛错退出。

写代码时常常有这样的需求:进入一段逻辑前打开了某个资源(文件、锁、网络连接),不管中间是正常走完、还是中途 return、还是抛了错误,最后都得把它关掉。手动在每个出口都写一遍清理,既啰嗦又容易漏。defer 就是为这个而生的。

1-1 defer 是什么

defer 语句把一段代码”存起来”,推迟到当前作用域要退出时才执行。它只声明”稍后做”,并不立刻执行。最典型的用处就是资源清理:

struct File {
    let name: String
    func readline() throws -> String? { nil }
    func close() {}
}
func exists(_ filename: String) -> Bool { true }
func open(_ filename: String) -> File { File(name: filename) }
func close(_ file: File) {}

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // 处理文件内容
        }
        // 这里作用域结束,defer 里的 close(file) 被执行
    }
}

open 之后立刻写 defer { close(file) },把”关闭”和”打开”写在相邻的两行,逻辑上成对出现,可读性极好。无论后面的 while 是正常跑完,还是 readline() 抛错跳出,只要离开这个 if 作用域,close(file) 就一定会被调用。

1-2 不管怎么退出都会执行

defer 最值得信赖的地方,是它”铁定执行”。普通清理代码如果写在 returnthrow 之后,就永远到不了;但 defer 块的代码是在作用域真正退出那一刻才跑,所以即使中间提前 return 或抛错,清理也不会被跳过。

func demo() throws {
    print("开始")
    defer { print("清理") }
    if true {
        return   // 即使这里提前返回
    }
    print("中间")
}
// 无论走哪条路,"清理"都会被打印

这正是它比”在每个出口手写清理”可靠的原因:你只需要写一次 defer,它就兜住了所有出口。处理文件描述符、释放手动分配的内存、解锁等场景,都用它最合适。

1-3 多个 defer 的 LIFO 顺序

如果在同一作用域里写了多个 defer,它们的执行顺序是后进先出(LIFO,栈式):后写的先执行,先写的最执行。

func order() {
    defer { print("第一") }
    defer { print("第二") }
    defer { print("第三") }
    print("正文")
}
// 输出顺序:正文、第三、第二、第一

“正文”先打印,然后三个 defer 反着来:第三、第二、第一。这个顺序很合理——想象成”开冰箱、开里层抽屉、放东西”,收尾时当然先关里层抽屉、再关冰箱。所以写多个 defer 时,按”与初始化相反”的顺序排,清理才正确。

Note

defer 块里不能包含会转移控制流的语句,比如 returnbreak,也不能在里面 throw。它的职责是”清理”,不是”改流程”。如果非要在退出前决定抛不抛错,那逻辑应写在 defer 之外。

1-4 与错误处理配合

defer 经常和上一章的错误处理搭档。比如一个会抛错的函数,无论成功还是失败,都要保证把打开的资源关掉:

struct Config { }
enum ConfigError: Error { case malformed }
func openConfig() -> Config { Config() }
func closeConfig(_ handle: Config) { }
func parse(_ handle: Config) -> Config? { nil }

func readConfiguration() throws -> Config {
    let handle = openConfig()
    defer { closeConfig(handle) }
    guard let config = parse(handle) else {
        throw ConfigError.malformed
    }
    return config
    // 不论 guard 抛错还是正常返回,closeConfig 都会执行
}

defer 把”关资源”这件事从”成功/失败两条分支”里抽出来,集中到一处,既不重复也不遗漏。这正是它存在的最大价值:让清理逻辑和主逻辑解耦。

1-5 适用面与常见疑问

defer 的适用面远不止文件。任何”获取—使用—释放”的成对操作都适合它:加锁之后 defer { unlock() };手动分配内存后 defer { free(ptr) };打开数据库连接后 defer { close(connection) }。只要把”释放或还原”紧挨着”获取”写在一起,并用 defer 包起来,就能保证无论中间发生什么,清理都不会被漏掉。这比在每一个可能的出口都手抄一遍清理代码可靠得多。

有一点要提醒:defer 捕获的是它执行那一刻的变量值,而不是声明时的值。如果在 defer 里引用了某个变量,而该变量在 defer 真正执行前被改了,那么 defer 用到的是”退出时”的最新值。所以不要在 defer 里依赖”声明 defer 那一刻的快照”去做假设,否则会得到和你预期不同的结果。这个特性在需要按最终状态清理时反而有用,但前提是你清楚变量的变化。

还有一个常见疑问:defer 能不能用在循环里?可以,而且很有用。比如每次循环开头打开一个临时资源,defer 写在循环体里,那么每轮循环结束时都会各自执行一次清理,不会等到整个函数结束才一起清。这让”每轮独立清理”变得自然,不用手动在循环末尾逐个释放。记住 defer 的作用域是”它所在的那对大括号”,循环体本身就是一个作用域,所以每轮都会触发。

最后澄清一个边界:defer 只负责”推迟执行清理”,它不能替代真正的错误处理。如果某步操作失败需要上报、需要区分错误类型,那应该用 do-catchdefer 只是确保”不管成功失败,清理都做”。实际项目里两者常常配合:用 do-catch 决定怎么应对错误,用 defer 保证资源释放,各司其职。把清理逻辑从业务分支里抽出来交给 defer,代码会更短、更不容易在改业务逻辑时意外漏掉释放。

1-6 易错点速记

defer 适合一切”获取—使用—释放”的成对操作:加锁/解锁、开连接/关连接、分配/释放。把释放紧挨获取写在一起并用 defer 包住,比在每个出口手抄清理可靠得多。

两个要点:第一,defer 捕获的是”退出时”的变量最新值,不是声明 defer 那一刻的快照;第二,多个 defer 按后进先出执行,书写时按与初始化相反的顺序排最稳妥。defer 也能用在循环体里,每轮各自触发一次清理。

它不能替代错误处理:defer 只管”清理必做”,错误的分类与上报交给 do-catch。两者常配合,各司其职。一个反模式:把本该在 defer 里做的清理写死在某个 return 前,结果另一个分支忘了写,资源就漏了。凡是”无论怎么退出都要做”的事,交给 defer 才是正解。

1-7 小结

defer 把清理代码推迟到作用域退出前执行,且”无论如何退出都执行”,是资源管理的保险丝。多个 defer 按后进先出顺序运行,书写时按与初始化相反的顺序排列最稳妥。它不依赖你是否记得在每个出口写清理,而是从源头保证清理必发生。至此,闭包与可选类型、错误处理两大模块全部讲完。