Swift

闭包

全面解析 Swift 中的闭包特性,涵盖基础语法、内存管理、逃逸闭包以及并发环境下的 @Sendable 和隔离机制。

闭包是 Swift 中功能强大的自包含代码块,可以在代码中被传递和使用。Swift 6.2 进一步增强了闭包在并发环境下的安全性和表达能力。本文将深入探讨闭包的核心概念及其在现代 Swift 开发中的应用。

基础语法与简写

Swift 提供了简洁的闭包语法。系统会自动推断参数和返回值类型,允许省略 return 关键字,并支持尾随闭包语法。

let numbers = [5, 2, 9, 1, 7]

// 完整写法
let sorted1 = numbers.sorted(by: { (s1: Int, s2: Int) -> Bool in
    return s1 < s2
})

// 简写语法:类型推断、隐式返回、参数名称缩写
let sorted2 = numbers.sorted(by: { $0 < $1 })

// 尾随闭包:作为最后一个参数时可写在括号外
let sorted3 = numbers.sorted { $0 < $1 }

捕获列表与内存管理

闭包默认会强引用其捕获的外部变量。在类实例中使用闭包时,容易造成循环引用。通过捕获列表(Capture List),我们可以定义捕获规则来打破循环引用。

weak 与 unowned

  • weak:将捕获的引用变为弱引用,类型为可选值(Optional),引用对象释放后变为 nil
  • unowned:将捕获的引用变为无主引用,非可选值,若引用对象已释放,访问会导致崩溃。
class DataManager {
    var data: [String] = []
    var onUpdate: (() -> Void)?
    
    func setup() {
        // 使用 [weak self] 避免循环引用
        onUpdate = { [weak self] in
            guard let self else { return }
            print("Data count: \(self.data.count)")
        }
    }
    
    deinit {
        print("DataManager deinitialized")
    }
}

逃逸闭包 (@escaping)

当闭包作为参数传递给函数,但在函数返回后才被执行时,该闭包必须标记为 @escaping。这常见于异步操作 completion handler 中。

var completionHandlers: [() -> Void] = []

func performAsyncOperation(completion: @escaping () -> Void) {
    // 闭包被存储,稍后执行
    completionHandlers.append(completion)
}

func performSyncOperation(closure: () -> Void) {
    // 闭包在函数内直接执行,无需 @escaping
    closure()
}

自动闭包 (@autoclosure)

@autoclosure 允许将一个表达式自动封装为闭包。它不接受参数,被调用时返回表达式的值。这种特性常用于延迟求值,例如 assert 函数。

// condition 参数自动封装为闭包,仅在需要时执行
func logIfTrue(_ condition: @autoclosure () -> Bool, message: String) {
    if condition() {
        print(message)
    }
}

// 调用时直接传入表达式
logIfTrue(2 > 1, message: "Condition is true")

Swift 6 并发与闭包

Swift 6 引入了严格的并发检查,闭包在并发环境下的行为受到了更严格的约束。

@Sendable 闭包

@Sendable 标记表示闭包可以在并发域之间安全传递。这要求闭包捕获的所有值也必须是 Sendable 的。

// 定义一个 Sendable 闭包类型
let concurrentOperation: @Sendable (Int) -> Void

concurrentOperation = { id in
    print("Processing \(id)")
}

// Task 初始化器接受 @Sendable 闭包
Task {
    concurrentOperation(1)
}

如果闭包捕获了非 Sendable 的可变状态,编译器会报错,从而防止数据竞争。

闭包的 Actor 隔离

Swift 6 中,闭包会根据上下文继承 Actor 隔离。

  • 非隔离闭包:默认情况下,普通闭包不继承 Actor 隔离。
  • 隔离继承:使用 @MainActor 或其他 Actor 标记的函数中定义的闭包,在某些情况下会继承隔离性。
@MainActor
class ViewModel {
    var state = "Idle"
    
    func update() {
        // Task 继承 @MainActor 隔离,可以安全访问 state
        Task {
            state = "Updating"
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            state = "Done"
        }
    }
    
    func performDetached() {
        // detached 任务不继承隔离,访问 state 需要 await
        Task.detached {
            await self.updateState("Detached")
        }
    }
    
    func updateState(_ newState: String) {
        state = newState
    }
}

隔离断言 (#isolation)

Swift 6.2 引入了 #isolation 宏,用于在运行时获取当前闭包的隔离上下文。这对于调试和编写通用的并发代码非常有用。

func checkIsolation() {
    print("Current isolation: \(String(describing: #isolation))")
}

Task { @MainActor in
    // 输出 MainActor 实例
    checkIsolation()
}

总结

Swift 的闭包不仅保持了语法的简洁性,更通过 @Sendable 和 Actor 隔离机制,成为了并发编程的安全基石。掌握捕获列表、逃逸闭包以及并发特性,是编写高质量 Swift 代码的关键。

在 GitHub 上编辑

上次更新于