有用的知识又增加了:如何让代码全面适配 Swift 6

有用的知识又增加了:如何让代码全面适配 Swift 6_第1张图片

概述

自从 Swift 并发模型首次引入 async 和 await 关键字以来,我就迫不及待的开始使用它们来进行异步代码的开发了。随着时间的推移,Swift 并发模型变得越来越强大,它通过让 Swift 编译器识别潜在问题,提供了可靠的数据竞争安全保障。

有用的知识又增加了:如何让代码全面适配 Swift 6_第2张图片

然而,在切换到 Swift 6 版本后,大家面对代码中自动生成的所有警告和错误可能会显得束手无策。这里,我们将分享一些在代码库中适配 Swift 6 严格并发模式(Strict Concurrency)的小技巧,以充分利用代码安全性所带来的优势。

在本篇博文中,您将学到如下内容:

  • 概述
  • 1. Swift 6 专治数据竞争之祸的始末原由
  • 2. 结构!结构!结构
  • 3. 你就是并发中的“主角(Actor)”!
  • 总结

我们的代码常需要纳新吐故,而本篇的谆谆善诱必将使小伙伴们受益匪浅、满载而归!那还等什么呢?让我们马上开始 Adopting Swift 6 之旅吧!

Let‘s go!!!


1. Swift 6 专治数据竞争之祸的始末原由

在 Swift 6 的适配之路上,编译器会频繁抱怨有关可发送性(sendability) 这一问题,因为这是数据竞争的主要原因。

数据竞争发生在一段代码写入内存,而同时另一段代码读取同一块内存的引用时。在这种情况下,我们“可耐”的 App 可能会因为奇怪的 EXC_BAD_ACCESS错误而榱栋崩折。

通常,数据竞争发生在多个线程共享一个类的实例对象时,并且其中至少有一个线程对其进行了写入操作。

为了避免这种情况,我们在代码库中应该尽量减少类的使用。下面就让我们来看看具体如何躬行实践吧。

2. 结构!结构!结构

如果某个对象是不可修改的,那就不可能发生数据竞争。 这一原则鼓励我们尽可能多地使用结构体。

例如,我们可以将所有数据类型定义为可发送(Sendable)且不可变(Immutable)的结构体:

public struct Statistics: Sendable, Hashable {
    public let value: Double
    public let interval: DateInterval
    
    public init(value: Double, interval: DateInterval) {
        self.value = value
        self.interval = interval
    }
}

在上面的示例代码中,我们将 Statistics 定义为 Sendable 结构体。在大多数情况下,结构体可以隐式推断其自身的可发送性,但当我们将其定义为 public 时,就需要显式地声明。

通过遵循 Sendable 协议,我们可以让编译器懂得:某个类型的实例可以安全地在线程之间共享。

不仅数据类型可以是结构体,我们也能将无状态(Stateless)的服务类型定义为结构体:

public struct HealthService: Sendable {
    private let store: HKHealthStore
    
    public init(store: HKHealthStore) {
        self.store = store
    }
    
    public var age: Int {
        guard
            let dateOfBirthComponents = try? store.dateOfBirthComponents(),
            let dateOfBirth = Calendar.current.date(from: dateOfBirthComponents)
        else {
            return 0
        }
        
        let age = Calendar.current.dateComponents([.year], from: dateOfBirth, to: .now)
        return age.year ?? 0
    }
}

结构体不仅“本领高强”,而且创建成本也比类更低。在 Swift 6 中,小伙伴们可以怡然自得地在线程之间传递 Sendable 结构体,而不会遇到任何警告或数据竞争错误。因此,我们强烈建议尽可能优先将结构体整合到自己的代码库中去。

3. 你就是并发中的“主角(Actor)”!

虽说结构的优点众多,但整个应用的开发无法完全脱离引用类型。类(Class)非常适用于一个特定情形:它允许我们共享状态,而无需复制它。

在 App 中,一组视图可能依赖于单个共享状态,并需要在这些视图之间保持同步。不幸的是,结构体此时无法满足需求,因为每个视图都会获得结构体实例的一个副本,而这通常绝不是我们想要的结果。

我们在应用中应该尽量将类的使用限制在与视图相关( view-related)的场景中,例如:视图模型(View Model)或代理(Delegate)类型。由于它们与视图相关,因此我们需要使用 @MainActor 对它们进行修饰。

关于全局 Actor 的特性及其好处,小伙伴们可以参考我的 Swift 结构化并发之全局 Actor 趣谈 一文。

全局 Actor 是使类型隐式 Sendable 的另一种方式。当某个类型被隔离到一个全局 Actor 时,它存储的数据将不会被同时读写,这多亏了 Actor 的强大威力:

@MainActor @Observable final class Store<State, Action> {
    private(set) var state: State
    private let reduce: (State, Action) -> State
    
    init(
        initialState state: State,
        reduce: @escaping (State, Action) -> State
    ) {
        self.state = state
        self.reduce = reduce
    }
    
    func send(_ action: Action) {
        state = reduce(state, action)
    }
}

除了数据类型、无状态服务和视图相关的内容以外,我们的应用里还有一类对象属于有状态服务类型的范畴。比如:一个缓存搜索结果的搜索服务,它可能会被多个线程同时使用,并且需要维护缓存的并发访问状态。

actor SearchService {
    private var history = Tree<Food>()
    
    func search() async throws -> [Food] {
        // read history
        
        // make network request
        
        // mutate history
    }
}

这种类型是应用中潜在的数据竞争来源,但值得庆幸的是,通过使用单个 actor 关键字可以轻松化解它们于无形。正如大家已经知道的那样:actor 同样可以保护其状态,并允许互斥访问,从而消除数据竞争。


如果小伙伴们想了解更多关于 Swift 中 Actor 的知识,可以移步如下链接观赏精彩的内容:

  • Swift 搞定“Main actor-isolated property can not be referenced from a Sendable closure”编译错误
  • 深入理解 Swift 新并发模型中 Actor 的重入(Reentrancy)问题
  • SwiftUI async/await 并发代码提示 Non-sendable type cannot cross actor boundary 警告的解决

Swift 是一门优秀且性感的语言,它能够让我们撸出简洁且安全的代码。Swift 同样提供了一套全面的工具集,来帮助大家轻松实现自己内心深处的“小目标”,棒棒哒!


想要进一步系统地学习 Swift 开发的小伙伴们,可以来我的《Swift 语言开发精讲》专栏逛一逛哦:

有用的知识又增加了:如何让代码全面适配 Swift 6_第3张图片

  • 《Swift 语言开发精讲》

总结

在本篇博文中,我们讨论了如何让自己项目中的旧代码全面适配 Swift 6 的基本原则和一些小技巧,相信大家定能手到擒来。

感谢观赏,再会啦!

你可能感兴趣的:(Apple开发入门,Swift,6,结构化并发,数据竞争,Data,Race,Actor,结构和类,MainActor)