在 async/throwing 场景下优雅地使用 Swift 的 defer 关键字

原文:Using Swift's defer keyword within async and throwing contexts -- Swift by Sundell

在日常 Swift 开发中,我们经常需要在多出口的函数里做清理工作:关闭文件句柄、归还数据库连接、把布尔值复原......如果每个出口都手写一遍,既啰嗦又容易遗漏。

Swift 提供了 defer 关键字,让我们可以把"善后逻辑"一次性声明在当前作用域顶部,却延迟到作用域退出时才执行。

本文将结合错误抛出(throwing)与并发(async/await)两个典型场景,带你彻底吃透 defer 的用法与注意点。

defer 基础回顾

defer { ... } 中的代码,会等到当前作用域(函数、闭包、do 块......)即将退出时执行,无论退出路径是 return、throw 还是 break。

最小示例:

swift 复制代码
func demo() {
    defer { print("最后才打印") }
    print("先打印")
    // 函数返回前,defer 里的内容一定执行
}

同步 + throwing 场景:避免重复清理

想象一个 SearchService,它通过 Database API 查询条目,必须先 open、后 close:

❌ 传统写法:分支重复

swift 复制代码
actor SearchService {
    private let database: Database

    func loadItems(matching searchString: String) throws -> [Item] {
        let connection = database.connect()

        do {
            let items: [Item] = try connection.runQuery(
                .entries(matching: searchString)
            )
            connection.close()          // 成功路径
            return items
        } catch {
            connection.close()          // 失败路径
            throw error
        }
    }
}

问题:

  • 两处 close(),容易漏写。
  • 如果再加 returnguard,分支会更多。

✅ 利用 defer:把 close 写在 open 旁边

swift 复制代码
func loadItems(matching searchString: String) throws -> [Item] {
    let connection = database.connect()
    defer { connection.close() }          // 一次声明,处处生效

    return try connection.runQuery(.entries(matching: searchString))
}

优点:

  • 逻辑集中,一眼可见"成对动作"。
  • 任意新增提前退出(guardthrow)都不用再管 close()

⚠️ 注意执行顺序:

connect()runQuery() → 作用域结束 → close()

"延迟"并不代表"立刻",代码阅读时需要适应这种跳跃。

async/await 场景:状态复原与去重

并发代码里,defer 更显价值------异步函数可能在任意 await 点挂起并抛错,手动追踪所有出口几乎不现实。

复原布尔 flag

场景:防止重复加载,用 isLoading 标记。

❌ 传统写法:catch 里回写 flag

swift 复制代码
actor ItemListService {
    private let networking: NetworkingService
    private var isLoading = false

    func loadItems(after lastItem: Item) async throws -> [Item] {
        guard !isLoading else { throw Error.alreadyLoading }
        isLoading = true

        do {
            let request  = requestForLoadingItems(after: lastItem)
            let response = try await networking.performRequest(request)
            let items    = try response.decoded() as [Item]

            isLoading = false        // 成功路径
            return items
        } catch {
            isLoading = false        // 失败路径
            throw error
        }
    }
}

✅ defer 写法:一行搞定

swift 复制代码
func loadItems(after lastItem: Item) async throws -> [Item] {
    guard !isLoading else { throw LoadingError.alreadyLoading }
    isLoading = true
    defer { isLoading = false }       // 无论成功/失败都会执行

    let request  = requestForLoadingItems(after: lastItem)
    let response = try await networking.performRequest(request)
    return try response.decoded()
}

利用 Task + defer 做"去重"

需求:如果同一 lastItem.id 的加载任务已在进行中,直接等待现有任务,而不是重新发起。

swift 复制代码
actor ItemListService {
    private let networking: NetworkingService
    private var activeTasks: [Item.ID: Task<[Item], Error>] = [:]

    func loadItems(after lastItem: Item) async throws -> [Item] {
        // 1. 已有任务则等待
        if let existing = activeTasks[lastItem.id] {
            return try await existing.value
        }

        // 2. 创建新任务
        let task = Task {
            // 任务结束前一定清理字典
            defer { activeTasks[lastItem.id] = nil }

            let request  = requestForLoadingItems(after: lastItem)
            let response = try await networking.performRequest(request)
            return try response.decoded() as [Item]
        }

        // 3. 登记
        activeTasks[lastItem.id] = task
        return try await task.value
    }
}

要点:

  • 一旦 Task 结束(无论正常返回还是抛错),defer 把字典条目删掉,防止内存泄漏。
  • 因为 actor 的重入性(reentrancy),await 期间仍可接受新调用;通过字典+Task 实现"幂等"效果。

何时不要使用 defer

  • 逻辑需要严格顺序时:defer 会在作用域最后执行,若必须与中间语句保持先后关系,则不适合。
  • 过度嵌套:多层 defer 会让执行顺序难以一眼看出,阅读负担大。
  • 性能极端敏感:defer 本质是隐藏的 try/finally,有微小开销,但通常可以忽略。

小结 checklist

场景 是否推荐 defer 单一出口函数 ❌ 没必要 多出口、需清理资源 ✅ 强烈推荐 async/await 中状态复原 ✅ 强烈推荐 需要精确控制顺序 ❌ 慎用

一句话:defer 是"善后"利器,不是"流程"利器。

只要牢记"无论怎么退出,这段代码一定跑",就能把它用得恰到好处。

相关推荐
00后程序员张8 小时前
iOS 开发全流程需要哪些工具?编码、调试、构建、发布阶段选择
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
weixin_531670891 天前
Interlude起来:久坐提醒软件为什么需要登录?有没有完全本地运行的Mac休息提醒软件?
人工智能·macos·swift
2501_916008891 天前
Swift多环境配置:利用Scheme实现灵活的多环境管理
开发语言·ide·vscode·ios·个人开发·swift·敏捷流程
iFlyCai2 天前
iOS中的单例模式详解
ios·单例模式·objective-c·多线程·swift
weixin_531670892 天前
Interlude起来:Swift原生Mac软件有什么优势?原生Mac应用和Electron应用有什么区别?
人工智能·macos·mac·swift
k4m7v2pz2 天前
Swift Package Manager 在 macOS 26 上的三个编译错误排查指南
macos·spm·ai编程·xcode·swift·命令行工具
冯汉栩3 天前
Swift Control DateSelection(日期选择框)
ios·cocoa·swift
冯汉栩3 天前
Swift Control DashLineView(虚线)
开发语言·ios·swift
2501_915918413 天前
iOS 真机调试工具推荐,安装运行、性能分析有哪些工具
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
软泡芙4 天前
【Swift】Swift vs C# 核心语法对比指南
c#·ssh·swift