在 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 是"善后"利器,不是"流程"利器。

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

相关推荐
HarderCoder2 小时前
Swift Concurrency:彻底告别“线程思维”,拥抱 Task 的世界
swift
HarderCoder2 小时前
深入理解 Swift 中的 async/await:告别回调地狱,拥抱结构化并发
swift
HarderCoder3 小时前
深入理解 SwiftUI 的 ViewBuilder:从隐式语法到自定义容器
swiftui·swift
东坡肘子3 小时前
我差点失去了巴顿(我的狗狗) | 肘子的 Swift 周报 #098
swiftui·swift·apple
Swift社区15 小时前
Swift 实战:实现一个简化版的 Twitter(LeetCode 355)
leetcode·swift·twitter
HarderCoder15 小时前
当Swift Codable遇到缺失字段:优雅解决数据解码难题
swift
YungFan2 天前
iOS26适配指南之UIButton
ios·swift
麦兜*3 天前
【swift】SwiftUI动画卡顿全解:GeometryReader滥用检测与Canvas绘制替代方案
服务器·ios·swiftui·android studio·objective-c·ai编程·swift
Swift社区4 天前
Swift 实战:从数据流到不重叠区间的高效转换
开发语言·ios·swift