+ 我要发布
我发布的 我的标签 发现
浏览器扩展
斑点象@Edge

Swift 6.0 新特性详解

Swift 6 带来了大量关于并发的更新,开发团队为此付出了巨大的努力。最大的变化是完整的并发检查默认启用。如果你的代码中有很多并发相关的使用,那么你的代码要升级到 6.0 很可能需要进行一些调整。 Swift 6 进一步改进了并发检查,并且 Swift 团队表示它“移除了许多在 5.10 中的假数据竞争警告”。它还引入了几个有针对性的变化,使并发更容易使用。 其中,最重要的是 SE-0414,它定义了隔离区域,让编译器能够明确证明代码的不同部分可以并发运行。 这个变化的核心是现有的可发送性(sendability)概念。一个 Sendable 类型是指可以在并发环境中安全传递的类型,包括结构体、常量属性的 final 类、自动保护其可变状态的 Actors 等。 在 Swift 6 之前,如果你在一个 Actor 上有一个不可发送的值,并试图将其发送到另一个 Actor,你会收到并发检查警告。 你可以通过以下代码看到问题: ``` class User { var name = "Anonymous" } struct ContentView: View { var body: some View { Text("Hello, world!") .task { let user = User() await loadData(for: user) } } func loadData(for user: User) async { print("Loading data for \(user.name)…") } } ``` 在 Swift 6 之前,调用 loadData() 会抛出一个警告:“passing argument of non-sendable type 'User' outside of main actor-isolated context may introduce data races.” 在 Swift 6 之后,这个警告消失了:Swift 现在检测到代码实际上没有问题,因为 user 并没有被同时访问,因此不会发出警告。 这个变化意味着可发送对象现在要么符合 Sendable,要么不需要符合 Sendable 因为编译器可以证明它们被安全使用。 除了这个比较大的变动之外还有许多其他较小的改进: ``` SE-430[2] 增加了新的 sending 关键词,用于在隔离区域之间发送值。 SE-0423[3] 提高了与 Objective-C 操作时的并发支持。 SE-0420[4] 允许我们创建与调用者相同 Actor 隔离的异步函数。 ``` 全局变量的并发安全 SE-0412[5] 的提议要求全局变量在并发环境中是安全的。这适用于你项目中的全局变量: ``` var gigawatts = 1.21 ``` 也适用于类型中存储的静态变量: ``` struct House { static var motto = "Winter is coming" } ``` 这些数据可以随时随地访问,本质上是不安全的。要解决这个问题,你需要将变量转换为可发送常量,限制其到全局 Actor,如 @MainActor,或者如果你没有其他选择或者知道它在其他地方受保护,可以标记为非隔离的,但不推荐这种做法。 例如,以下都是允许的: ``` struct XWing { @MainActor static var sFoilsAttackPosition = true } struct WarpDrive { static let maximumSpeed = 9.975 } @MainActor var idNumber = 24601 // 不推荐,除非你确定它是安全的 nonisolated(unsafe) var britishCandy = ["Kit Kat", "Mars Bar", "Skittles", "Starburst", "Twix"] ``` 函数默认值的隔离 SE-0411[6] 这个提案改变了函数默认值,使它们具有与它们内部函数相同的隔离。例如,以下代码现在是允许的,以前会触发错误: ``` @MainActor class Logger { } @MainActor class DataController { init(logger: Logger = Logger()) { } } ``` 因为 DataController 和 Logger 都被限制在主 Actor 上,Swift 现在认为 Logger() 的创建也被限制在主 Actor 上,因此这很合理。 count(where:) 方法 SE-0220[7] 引入了一个新的 count(where:) 方法,它相当于先 filter() 再 count 的操作。直接使用这个方法可以少创建一个新数组,而且这个方法更加清晰简洁。 例如,创建一个测试结果的数组,并计算有多少个分数大于等于 85: ``` let scores = [100, 80, 85] let passCount = scores.count { $0 >= 85 } ``` 这计算数组中有多少名字以 "Terry" 开头: ``` let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"] let terryCount = pythons.count { $0.hasPrefix("Terry") } ``` 其实这个方法在 5 年前 5.0 发布的时候就有人提议过,但是当时没有好的算法被撤回了。 类型化的 throws SE-0413[8] 引入了指定函数可以抛出哪些类型错误的能力,称为“类型化的 throws”。这解决了 Swift 开发中一个小问题:即使我们已经捕获了所有可能的错误,也需要一个通用的 catch 子句。 举个例子,我们可以定义一个 CopierError 来捕获打印机缺纸的错误: ``` enum CopierError: Error { case outOfPaper } ``` 然后我们可以创建一个 Photocopier 结构体,它创建一些页面的副本。如果请求操作时没有足够的纸张,它可能会抛出错误,但我们使用 throws(CopierError) 来明确指出可能抛出的错误类型: ``` struct Photocopier { var pagesRemaining: Int mutating func copy(count: Int) throws(CopierError) { guard count <= pagesRemaining else { throw CopierError.outOfPaper } pagesRemaining -= count } } ``` 现在我们可以编写代码尝试复印,并捕获唯一可能抛出的错误: ``` do { var copier = Photocopier(pagesRemaining: 100) try copier.copy(count: 101) } catch CopierError.outOfPaper { print("请重新加纸") } ``` Pack 迭代 SE-0408[9] 提案引入了 pack 迭代,增加了循环遍历 Swift 5.9 中引入的参数 pack 功能的能力。pack 可以说是 Swift 最复杂的功能之一,但一些开发者提案来展示了这种功能的实用性,例如通过几行代码添加任意项数的元组比较: ``` func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool { for (left, right) in repeat (each lhs, each rhs) { guard left == right else { return false } } return true } ``` 关于这个功能,之前有文章介绍过,可以回去翻翻看。 详细讲讲 swift 5.9 出的新语法:参数包 非连续元素的集合操作 SE-0270[10] 引入了各种新方法,用于处理集合上的更复杂操作,如移动或移除非连续的多个项。这种变化是由一种新的类型 RangeSet 驱动的。 例如,我们可以创建一个考试成绩的学生数组: ``` struct ExamResult { var student: String var score: Int } let results = [ ExamResult(student: "Eric Effiong", score: 95), ExamResult(student: "Maeve Wiley", score: 70), ExamResult(student: "Otis Milburn", score: 100) ] ``` 我们可以获得得分大于等于 85 的学生索引的 RangeSet,如下所示: ``` let topResults = results.indices { student in student.score >= 85 } ``` 如果我们想访问这些学生,可以使用新的集合下标: ``` for result in results[topResults] { print("\(result.student) 得分 \(result.score)%") } ``` 导入声明的访问级别修饰符 SE-0409[11] 增加了为导入声明标记访问控制修饰符的能力。 什么意思呢?比如以后导入头文件你可以这么写了 internal import SomeLibrary。作用是 SomeLibrary 这个库只会在你的库内部使用,外部引入你的库的 App 看不到这个依赖库。 这将对开发 SDK 的团队避免意外泄露其依赖关系非常有用。 不可复制类型的升级 不可复制类型最早是在 Swift 5.9 中引入的,但在 Swift 6 中得到了几项升级。 不可复制类型允许我们创建具有唯一所有权的类型,可以根据需要使用 borrowing 或 borrowing 来传递。 例如,不可复制类型可以在泛型中使用: ``` struct Message: ~Copyable { var agent: String private var message: String init(agent: String, message: String) { self.agent = agent self.message = message } consuming func read() { print("\(agent): \(message)") } } func createMessage() { let message = Message(agent: "Ethan Hunt", message: "你需要从摩天大楼上滑下。") message.read() } ``` createMessage() 在这段代码中,编译器强制 message.read() 只能被调用一次,因为它 consumes(消耗)了这个对象。 加入 128 位整数类型 SE-0425[12] 提案引入了 Int128 和 UInt128。 怎么说呢,虽然这些类型的 API 可能很少有开发者使用(它的最大值为 170,141,183,460,469,231,731,687,303,715,884,105,727),但它们的引入标志着 Swift 在处理大整数方面的进步。 BitwiseCopyable SE-0426[13] 引入了一个新的 BitwiseCopyable 协议,其唯一目的是允许编译器为符合的类型创建更多优化的代码。 大部分情况下,你都不需要显性来启用 BitwiseCopyable 支持。因为 Swift 会自动将其应用于你创建的大多数结构体和枚举,只要它们包含的所有属性也是按位可拷贝的,比如你的类型中的属性类型为整数、浮点数、Bool 等等。 如果某个类型需要禁用 BitwiseCopyable ,可以通过添加 ~BitwiseCopyable 关键字来执行此操作,例如: ``` @frozen public enum CommandLine : ~BitwiseCopyable { } ``` 除了以上新功能,Swift 6.0 还包含一些其他的提案: ``` SE-0364[14]:外部类型的追溯一致性警告 SE-0415[15]:功能体宏 SE-0419[16]:Swift 回溯 API ```