讲讲 swift 中 defer 的实现原理和使用场景

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

在 Swift 中,defer 语句提供了一种便捷的方式来编写在当前作用域退出时执行的代码。这对于资源管理非常有用,比如关闭文件句柄、释放手动分配的内存等。今天来探讨一下 defer 的用法,并结合 Swift 官方源码来解析其实现原理。

defer 的用法

defer 语句让你能够在即将离开当前作用域时执行一些清理工作。不论是由于 return 语句提前退出,还是抛出了一个错误,亦或是简单地到达作用域的末尾,defer 块中的代码都将被执行。

下边是一些具体的使用场景:

1. 锁的获取与释放

在多线程编程中,锁是一种常见的同步机制。使用 defer 来释放锁可以保证即使在发生错误或提前返回的情况下,锁也能被正确释放。

csharp 复制代码
func safeMethod() {
    lock.lock()
    defer {
        lock.unlock()
    }

    // 执行需要同步的代码
    // 如果这里有 return 或抛出异常,defer 仍然保证锁被释放
}

2. 数据库操作

在进行数据库操作时,通常需要在操作结束后关闭数据库连接或事务。使用 defer 可以确保这些清理工作在各种退出路径上都能被执行。

go 复制代码
func updateDatabase() {
    let connection = database.connect()
    defer {
        connection.close()
    }

    // 执行数据库更新操作
    // 不管操作成功还是失败,连接都会被关闭
}

3. 文件处理

在处理文件时,确保文件在不再需要时被关闭是很重要的。defer 语句可以帮助我们达到这个目的。

swift 复制代码
func processFile(path: String) throws {
    let file = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
    defer {
        file.closeFile()
    }

    // 读取并处理文件内容
}

实现原理

要深入理解 defer 的工作原理,我们需要查看 Swift 的源码。在 Swift 项目的 GitHub 仓库中,有一个文件 Defer.h,其中定义了 defer 相关的实现细节。

arduino 复制代码
#ifndef SWIFT_BASIC_DEFER_H
#define SWIFT_BASIC_DEFER_H

#include "llvm/ADT/ScopeExit.h"

namespace swift {
  namespace detail {
    struct DeferTask {};
    template<typename F>
    auto operator+(DeferTask, F &&fn) ->
        decltype(llvm::make_scope_exit(std::forward<F>(fn))) {
      return llvm::make_scope_exit(std::forward<F>(fn));
    }
  }
}


#define DEFER_CONCAT_IMPL(x, y) x##y
#define DEFER_MACRO_CONCAT(x, y) DEFER_CONCAT_IMPL(x, y)

#define SWIFT_DEFER                                                            \
  auto DEFER_MACRO_CONCAT(defer_func, __COUNTER__) =                           \
      ::swift::detail::DeferTask() + [&]()

#endif // SWIFT_BASIC_DEFER_H

这段代码是用 C++ 写的,但它揭示了 defer 的核心机制。简单解释一下上边的代码:

  • SWIFT_DEFER 宏首先通过 DEFER_MACRO_CONCAT 宏和 __COUNTER__ 宏生成一个唯一的变量名

  • __COUNTER__ 是编译器提供的计数器,保证每个 SWIFT_DEFER 实例的变量名都是唯一的

  • 然后,它创建了一个 swift::detail::DeferTask 的实例,并使用+操作符重载将其与一个 lambda 表达式相加。这个 lambda 表达式是在SWIFT_DEFER 宏后面的代码块中定义的,它是在作用域结束时需要执行的代码。

  • 通过+操作符重载,返回了一个由 llvm::make_scope_exit 创建的对象,该对象会在其析构函数中执行 lambda 表达式。

  • 这个返回的对象被赋值给了一个局部变量,当这个局部变量的作用域结束时(即离开 SWIFT_DEFER 所在的作用域时),它的析构函数会自动被调用,从而执行之前定义的 lambda 表达式。

  • 每个 SWIFT_DEFER 实例化的对象都遵循 C++ 局部变量的生命周期规则,最后声明的局部变量会首先被销毁(LIFO 后进先出)。

了解了以上实现原理,我们来看一些特殊场景:

1. 一个作用域中有多个 defer 时

上边说了局部变量的销毁是 LIFO 后进先出的,所以当有多个 defer 时,最后一个会先执行:

go 复制代码
func complexOperation() {
    defer { print("清理操作 1") }
    defer { print("清理操作 2") }
    print("执行某些操作")
}
// 输出:
// 执行某些操作
// 清理操作 2
// 清理操作 1

2. 在 for 循环中使用

在循环中使用 defer 时,每次迭代都会创建一个新的 defer 块。这意味着 defer 中的代码会在每次迭代结束时执行,而不是在整个循环结束后。

swift 复制代码
for item in collection {
    defer { print("处理 \(item)") }
    if item.shouldSkip {
        continue
    }
    // 处理项
}

在这个例子中,即使 continue 语句跳过了某些迭代,每个 defer 也都会在其对应的迭代结束时执行。

3. 在 defer 中可以用 return 吗?

答案是不能 ,如果在 defer 中使用 return 语句,编译器会直接报错 'return' cannot transfer control out of a defer statement,这是因为 defer 中的代码是在作用域结束后执行的,也就是说,执行到 defer 的时候,函数已经 return 过了。

4. 在一个作用域下只写一个 defer 可以吗?

答案是可以,但没必要 ,因为如果作用域下没有其他代码,defer 下的代码会立即执行,因此 defer 也就失去了意义,硬要这么写的话编译器会给个警告,但不会报错:

go 复制代码
func doSoming() {
    defer {
        print("doSoming")
    }
}

结论

defer 是 Swift 中一个非常有用的特性,它简化了资源管理和清理工作的代码编写。

在实际开发中,合理利用 defer 不仅可以减少错误,还能使代码更加清晰和易于维护。除此之外 defer 还是一个经常在面试中被问到的知识点,希望通过本篇文章能帮助你更好地理解和使用 defer 语句。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 "iOS新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

相关推荐
大熊猫侯佩3 小时前
Swift 6.2 列传(第十三篇):香香公主的“倾城之恋”与优先级飞升
swift·编程语言·apple
程序员老刘6 小时前
Flutter 3.38 30天发6个版本,Google 程序员的头发还好吗?
flutter·客户端
专业开发者8 小时前
调试 iOS 蓝牙应用的新方法
物联网·macos·ios·cocoa
磊少工作室_CTO10 小时前
鸿蒙Next —— 状态管理实践
harmonyos·mvvm·客户端
1024小神10 小时前
Swift配置WKwebview加载网站或静态资源后,开启调试在电脑上debug
swift
tangbin58308513 小时前
iOS Swift 可选值(Optional)详解
前端·ios
卷心菜加农炮1 天前
基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署
ios
北极象1 天前
千问大模型接入示例
ios·iphone·qwen
ipad协议开发2 天前
企业微信 iPad 协议应用机器人开发
ios·企业微信·ipad
kkoral2 天前
基于MS-Swift 为 Qwen3-0.6B-Base 模型搭建可直接调用的 API 服务
python·conda·fastapi·swift