• Swift 6.0史诗级升级 新特性都有哪些?
  • 发布于 2个月前
  • 359 热度
    0 评论
  • 耀国
  • 0 粉丝 45 篇博客
  •   
前言
2024 年是 Swift 语言的十周年。在过去的五年里,Swift 没有进行过重大版本更新,而是从 5.0 到 5.10,迭代了 10 个小版本,5.0 到 6.0 Swift 花了 5 年时间,相比其他编程语言这样的情况其实并不罕见,像 Python 3 和 PHP 6 都经历了漫长的开发周期。今天,让我们来看看 Swift 6 中的具体变化吧。

完整并发默认启用
Swift 6 带来了大量关于并发的更新,开发团队为此付出了巨大的努力。最大的变化是完整的并发检查默认启用。如果你的代码中有很多并发相关的使用,那么你的代码要升级到 6.0 很可能需要进行一些调整。Swift 6 进一步改进了并发检查,并且 Swift 团队表示它“移除了许多在 5.10 中的假数据竞争警告”。它还引入了几个有针对性的变化,使并发更容易使用。

其中,最重要的是 SE-0414[1],它定义了隔离区域,让编译器能够明确证明代码的不同部分可以并发运行。这个变化的核心是现有的可发送性(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 因为编译器可以证明它们被安全使用。

除了这个比较大的变动之外还有许多其他较小的改进:
1.SE-430[2] 增加了新的 sending 关键词,用于在隔离区域之间发送值。
2.SE-0423[3] 提高了与 Objective-C 操作时的并发支持。

3.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
// 堆代码 duidaima.com
// 不推荐,除非你确定它是安全的
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
结语
Swift 6 的更新带来了许多令人兴奋的新功能和改进,特别是在并发和类型化错误处理方面。尽管 Swift 6 还未正式发布,但我们可以期待这些新特性将使开发更加高效和可靠,当然也可以通过安装 Xcode 15 Beta 来提前体验。通过这些更新,Swift 6 将继续推动开发者在创建更安全、更高效的代码方面前进。让我们期待 Swift 6 的正式发布,并在项目中充分利用这些新特性!你对 Swift 6 的看法是什么?请在评论区告诉我们。

参考资料
[1]SE-0414: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md
[2]SE-430: https://github.com/apple/swift-evolution/blob/main/proposals/0430-transferring-parameters-and-results.md
[3]SE-0423: https://github.com/apple/swift-evolution/blob/main/proposals/0423-dynamic-actor-isolation.md
[4]SE-0420: https://github.com/apple/swift-evolution/blob/main/proposals/0420-inheritance-of-actor-isolation.md
[5]SE-0412: https://github.com/apple/swift-evolution/blob/main/proposals/0412-strict-concurrency-for-global-variables.md
[6]SE-0411: https://github.com/apple/swift-evolution/blob/main/proposals/0411-isolated-default-values.md
[7]SE-0220: https://github.com/apple/swift-evolution/blob/master/proposals/0220-count-where.md
[8]SE-0413: https://github.com/apple/swift-evolution/blob/main/proposals/0413-typed-throws.md
[9]SE-0408: https://github.com/apple/swift-evolution/blob/main/proposals/0408-pack-iteration.md
[10]SE-0270: https://github.com/apple/swift-evolution/blob/main/proposals/0270-rangeset-and-collection-operations.md
[11]SE-0409: https://github.com/apple/swift-evolution/blob/main/proposals/0409-access-level-on-imports.md
[12]SE-0425: https://github.com/apple/swift-evolution/blob/main/proposals/0425-int128.md
[13]SE-0426: https://github.com/apple/swift-evolution/blob/main/proposals/0426-bitwise-copyable.md
[14]SE-0364: https://github.com/apple/swift-evolution/blob/main/proposals/0364-retroactive-conformance-warning.md
[15]SE-0415: https://github.com/apple/swift-evolution/blob/main/proposals/0415-function-body-macros.md
[16]SE-0419: https://github.com/apple/swift-evolution/blob/main/proposals/0419-backtrace-api.md
用户评论