Swift 中 @preconcurrency 修饰符使用浅谈

概述

Swift 6.0 与我们越来越近了,如何将旧范儿的并发代码装换为严格遵守 Swift 6.0 并发模型( Strict Concurrency)的新代码,这往往使得秃头码农们又要多抓掉几根头发了。

所以,为了最大限度的保持新旧两个并发世界暂时的"和平共处",苹果特地推出了 @preconcurrency 修饰符让我们能得以优雅的"樽前月下"。

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

  • 概述
  • [1. @preconcurrency 是干啥用的?](#1. @preconcurrency 是干啥用的?)
  • [2. 导入中的 @preconcurrency](#2. 导入中的 @preconcurrency)
  • [3. 类型修饰中的 @preconcurrency](#3. 类型修饰中的 @preconcurrency)
  • [4. 一些使用提示](#4. 一些使用提示)
  • 总结

闲言少叙,让我们马上开始 Swift 并发大冒险吧!

Let's go!!!😉


1. @preconcurrency 是干啥用的?

从 Swift 5.6(正式支持 @preconcurrency 的第一个 Swift 编译器版本)开始,苹果为 Swift 语言引入了@preconcurrency 修饰符。

当项目使用严格并发检查模式(strict concurrency checks)时我们可以使用它来让大量产生的编译器警告和错误"通通闭嘴"。

@preconcurrency 修饰符可以应用在以下 4 种语境中:

  • functions
  • types
  • protocols
  • imports

那么接下来就让我们在这些语境中挑一些比较典型的使用情况和大家一起聊聊,以便充分了解 @preconcurrency 是如何帮助我们在启用严格并发检查时规避一些"细枝末节"的,即使暂时我们还不能更新所有的依赖项。

2. 导入中的 @preconcurrency

在我们导入第三方模块时,有时编译器会"贴心"的做出如下提示:

swift 复制代码
import MyModule

这个"错误"告诉我们:我们正在导入一个似乎还没有完全遵守现代并发规则的模块。由于这可能不是小伙伴们能够掌控的模块,于是乎 Xcode 提供了"消除"来自该模块严格并发警告和错误的能力。

我们可以通过在导入语句中添加 @preconcurrency 修饰符来达成此目的:

swift 复制代码
@preconcurrency import MyModule

通过这样一番操作,Xcode 将明了与来自 MyModule 类型相关的任何警告都应该被"抑制"。

如果 MyModule 不是我们自己的模块(没有源代码),那么消除该警告是非常有意义的,因为无论如何我们都无法修复它们。

值得注意的是:这样做不会"抑制"与 MyModule 中可发送(Sendable)或最新现代并发代码相关的警告。

因此,如果我们在用 @preconcurrency 标记的模块上看到与并发相关的警告时,大家一定希望随后能够在适当的时候修复这些警告,因为此时我们有点像"掩耳盗铃"的撸码者。

3. 类型修饰中的 @preconcurrency

另一种可能的情况是,我们正在处理一个采用 Swift 新并发模式的模块,并且已经修复了警告。

如果是这样的话,我们可能需要在一些声明中添加 @preconcurrency 修饰符,以确保依赖于我们模块的代码不会失去兼容性("被打断")。

这意味着采用 Swift 新并发检查会导致我们的模块 ABI 接口发生变化,如果旧代码不遵循 Swift 新并发范式,那么它们可能无法使用你无与伦比的模块。

如果小伙伴们正处于这种情况,我们可能已经转换了一些旧代码:

swift 复制代码
public class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  func loadBooks() {
    // 加载书籍
  }
}

到如下新代码:

swift 复制代码
@MainActor
public final class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  public func loadBooks() {
    // 加载书籍
  }
}

此时,倘若我们编写了一些使用 CatalogViewModel 类的并发代码,它们可能看起来有点像这样:

swift 复制代码
class TestClass {
  func run() {
    let obj = CatalogViewModel()
    obj.loadBooks()
  }
}

遗憾的是,上面的代码会被编译器"大爆粗口":

Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context.

Call to main actor-isolated instance method 'loadBooks()' in a synchronous nonisolated context.

这是因为将 @MainActor 添加到模块中的类中会使我们无法使用视图模型(View Model),除非我们自己将指令流分发到主线程上去。

为了与 CatalogViewModel 顺畅交互,我们需要更新项目以使用 @MainActor。不过,这通常会滚雪球般地导致越来越多的代码被修改,这会使我们模块中的更改严重的"物是人非",讨厌的不要不要的。

这时,一种解决之道就是可以将 @preconcurrency 应用到我们的视图模型上,以允许尚未更新的代码与我们的视图模式达成"良好的共识",就好像它从未被用 @MainActor 修饰过一样:

swift 复制代码
@preconcurrency @MainActor 
public final class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  public func loadBooks() {
    // 加载书籍
  }
}

注意:以上仅适用于未启用严格新并发检查的项目哦。


在为我们整个类应用 @preconcurrency 修饰后,编译器将该类的并发检查设置为最小(minimal)的等级,就好像 @MainActor 修饰不存在一样。如果使用严格的并发检查,编译器仍然会由于没有将 CatalogViewModel 与 @MainActor 一起使用而发出错误。

4. 一些使用提示

综上所示,我们使用 @preconcurrency 修饰符可以将旧模块导入到新代码中,并允许在旧项目中使用新代码。随着 Swift 6 的发布临近,这是一个逐步采用严格并发的好开头。

当我们导入尚未更新以实现严格并发的模块时,向导入语句中添加@preconcurrency 是非常有用滴。

另外,在注释为 @Sendable、@MainActor 或以其它方式更新的声明中添加@preconcurrency 修饰符,使其无法在非并发代码中使用,这对库作者来说也具有非同一般的意义。

现在,我们彻底搞懂了 @preconcurrency 的作用,大家的新并发功力愈发的炉火纯青了!小伙伴们还不赶紧给自已一个大大的赞吧!

总结

在本篇博文中,我们介绍了 Swift 语言中 @preconcurrency 修饰符的作用,以及它在一些应用语境中"雪中送炭"的具体示例。

感谢观赏,再会!😎

相关推荐
依旧风轻7 小时前
Witness Table 的由来
ios·swift·witness table
依旧风轻7 小时前
Swift 中的方法调用机制
ios·swift·func·v-table·witness table
君陌笑1 天前
iOS-小说阅读器功能拆分之笔记划线
ios·swift
FreeCultureBoy2 天前
Swift 与 SwiftUI 学习系列:变量与常量篇 🚀
swiftui·xcode·swift
FreeCultureBoy2 天前
Swift 与 SwiftUI 学习系列: print 函数详解 🚀
swiftui·xcode·swift
依旧风轻2 天前
使用AES加密数据传输的iOS客户端实现方案
ios·swift·aes·network
依旧风轻3 天前
精确计算应用的冷启动耗时
ios·swift·cold start
hzgisme4 天前
iOS 视图实现渐变色背景
ios·cocoa·swift
大熊猫侯佩4 天前
Swift 中强大的 Key Paths(键路径)机制趣谈(上)
swift·序列·语法糖·键路径·keypath·转换为方法·协议扩展