讲讲 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新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

相关推荐
Geeker551 小时前
适用于 Windows 11/10/8/7/Vista/XP 的最佳免费分区软件
android·windows·ios·华为·智能手机·笔记本电脑·iphone
依旧风轻5 小时前
使用AES加密数据传输的iOS客户端实现方案
ios·swift·aes·network
wahaha1316811 小时前
ios项目pod成功,但是build里面没有生成对应的module
ios
童真的烂漫11 小时前
error: Sandbox: rsync.samba in Xcode project
ios
依旧风轻15 小时前
精确计算应用的冷启动耗时
ios·swift·cold start
#摩斯先生1 天前
iOS 真机打包,证书报错No signing certificate “iOS Distribution” found
ios
花生君1 天前
如何使用Xcode查看iOS APP客户端日志
ios·cocoa·xcode
zhaocarbon1 天前
SSZipArchive 解压后 中文文件名乱码问题
ios
KillerNoBlood1 天前
Objective-C语法基础
ios·objective-c