• Swift中defer的用法
  • 发布于 1个月前
  • 60 热度
    0 评论
  • 远行de风
  • 0 粉丝 26 篇博客
  •   
前言
在 Swift 中,defer 语句提供了一种便捷的方式来编写在当前作用域退出时执行的代码。这对于资源管理非常有用,比如关闭文件句柄、释放手动分配的内存等。今天来探讨一下 defer 的用法,并结合 Swift 官方源码来解析其实现原理。

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

下边是一些具体的使用场景:
1. 锁的获取与释放
在多线程编程中,锁是一种常见的同步机制。使用 defer 来释放锁可以保证即使在发生错误或提前返回的情况下,锁也能被正确释放。
func safeMethod() {
    lock.lock()
    defer {
        lock.unlock()
    }

    // 执行需要同步的代码
    // 如果这里有 return 或抛出异常,defer 仍然保证锁被释放
}
2. 数据库操作
在进行数据库操作时,通常需要在操作结束后关闭数据库连接或事务。使用 defer 可以确保这些清理工作在各种退出路径上都能被执行。
func updateDatabase() {
    let connection = database.connect()
    defer {
        connection.close()
    }
     // 堆代码 duidaima.com
    // 执行数据库更新操作
    // 不管操作成功还是失败,连接都会被关闭
}
3. 文件处理
在处理文件时,确保文件在不再需要时被关闭是很重要的。defer 语句可以帮助我们达到这个目的。
func processFile(path: String) throws {
    let file = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
    defer {
        file.closeFile()
    }

    // 读取并处理文件内容
}
实现原理
要深入理解 defer 的工作原理,我们需要查看 Swift 的源码。在 Swift 项目的 GitHub 仓库中,有一个文件 Defer.h,其中定义了 defer 相关的实现细节。
#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 时,最后一个会先执行:
func complexOperation() {
    defer { print("清理操作 1") }
    defer { print("清理操作 2") }
    print("执行某些操作")
}
// 输出:
// 执行某些操作
// 清理操作 2
// 清理操作 1
2. 在 for 循环中使用
在循环中使用 defer 时,每次迭代都会创建一个新的 defer 块。这意味着 defer 中的代码会在每次迭代结束时执行,而不是在整个循环结束后。
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 也就失去了意义,硬要这么写的话编译器会给个警告,但不会报错:
func doSoming() {
    defer {
        print("doSoming")
    }
}

结论
defer 是 Swift 中一个非常有用的特性,它简化了资源管理和清理工作的代码编写。在实际开发中,合理利用 defer 不仅可以减少错误,还能使代码更加清晰和易于维护。除此之外 defer 还是一个经常在面试中被问到的知识点,希望通过本篇文章能帮助你更好地理解和使用 defer 语句。
用户评论