hiccLoghicc log by wccHipo日志

Swift 6.0新特性

toc

Intro

完全并发默认开启

Swift 6 针对并发做了不少的更新,其中最大的就是默认打开了完全并发检查,之前可选的就是能给项目更多的时间来调整代码。

Swift 6对并发检查做了进一步的改进,移除了大量5.10中误报数据竞争的告警。

最大的引入了SE-0414,定义了isolation region,来让编译器能够最终决定代码中不同部分能够并发执行。

这里核心的点在已存在的概念可发送性sendability,struct,常量属性的final clalss,actor 等这些Sendable类能够在并发环境中安全传递。

在Swfit 6之前的版本中,如果一个non-sendable的值从一个actor传递到另一个actor,会报一个并发检查的告警。例如下面,SwiftUI View body 在主actor上执行,SwiftUI view自己并不在主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之后t能够检测出,这里并没有多个user数据获取,因此也就不会有并发告警。

可发送的值,要么遵循Sendable ,要么就是Swift证明了不需要遵循。简化了并发编程。

这里还有其他的改进:

  • SE-430 增加了新的sending关键词,来让我们在隔离区(isolation region)传送值的时候使用。
    • This proposal extends region isolation to enable the application of an explicit sending annotation to function parameters and results. A function parameter or result that is annotated with sending is required to be disconnected at the function boundary and thus possesses the capability of being safely sent across an isolation domain or merged into an actor-isolated region in the function's body or the function's caller respectively.
  • SE-0423 改进了操作Objective-C框架时候的并发能力
  • SE-0420 容许我们让async函数隔离在调用者相同的actor。

还有些改动是,之前藏在特性开关中的。 例如,SE-0401移除了Swift 5.5引入的一个特性:property wrapper的actor推断。

之前,任何struct或者class使用了@MainActor装饰的属性包裹器都会自动变成@MainActor。如果你在SwiftUI中使用了@StateObject或者@ObservedObject整个view都会变成@MainActor

例如,下面的view model,装饰@MainActor是一个好的做法:

@MainActor class ViewModel: ObservableObject { func authenticate() { print("Authenticating…") } }

在Swfit 6之后,你如果要在SwiftUI中使用@StateObject, 你必须也需要将view 也装饰为@MainActor

@MainActor struct LogInView: View { @StateObject private var model = ViewModel() var body: some View { Button("Hello, world", action: startAuthentication) } func startAuthentication() { model.authenticate() } }

之前版本的Swift中不需要,是因为view中使用了@StateObject,整个view就自动赋予了@MainActor

另一个Swift 6中被开启的是SE-0412, 需要全局变量在并发环境中安全。

例如,全局的变量:

var gigawatts = 1.21

或者存储在类型中的静态变量

struct House { static var motto = "Winter is coming" }

这些数据可以在任何时候都被获取到,这样在并发环境总就会不安全。 解决这个问题:

  • 转换为sendable 常量,或者限制在global actor,例如@MainActor中。
  • 上述不行,或者有其他的保守手段,可以标注为nonisolated.
struct XWing { @MainActor static var sFoilsAttackPosition = true } struct WarpDrive { static let maximumSpeed = 9.975 } @MainActor var idNumber = 24601 // Not recommended unless you're certain it's safe nonisolated(unsafe) var britishCandy = ["Kit Kat", "Mars Bar", "Skittles", "Starburst", "Twix"]

还有一个开启的是SE-0411,函数的默认值和函数本身在一样的隔离区isolation。

例如,下面列子在之前版本是报错的。

@MainActor class Logger { } @MainActor class DataController { init(logger: Logger = Logger()) { } }

因为DataControllerLogger 都限制在主actor,Swift现在认为创建Logger()也需要局限在主actor,这确实合理的。

更多Swift并发的知识,可以查看https://www.massicotte.org/

count(where:)

SE-0220,引入了count(where:)的新方法。相当于filter()只是用来计数。这点很方便免去了很多时候为了计数而创建临时数组。

let scores = [100, 80, 85] let passCount = scores.count { $0 >= 85 }
let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"] let terryCount = pythons.count { $0.hasPrefix("Terry") }

这个方法,所有遵循Sequence的类型都能使用。例如,set和字典dic也能使用。

类型化抛错

SE-0413引入了一种称之为“Typed throws”能力,可以指定函数抛出错误的类型。这就解决了Swift 错误处理中比较烦人的点:即使我们已知了所有可能的错误,我们还是一个通用的捕获错误的语句。

例如,我们可以定义一个CopierError的错误。

enum CopierError: Error { case outOfPaper }

我们创建一个Photocopier结构体有一个copy方法。我们可以在没有足够纸张的时候不抛出通用的throws而是指定错误throws(CopierError)

struct Photocopier { var pagesRemaining: Int mutating func copy(count: Int) throws(CopierError) { guard count <= pagesRemaining else { throw CopierError.outOfPaper } pagesRemaining -= count } }

注:throws表示任意类型的错误,throws(OneSpecificErrorType)throws(A, B, C) 表示指定错误。

捕获错误也会简单很多:

do { var copier = Photocopier(pagesRemaining: 100) try copier.copy(count: 101) } catch CopierError.outOfPaper { print("Please refill the paper") }

升级之后,throws(any Error) 等价于throwsthrow(Never)等于不抛出错误,这个可能有点费解,但是对`rethrows`来说就很明确了,函数会抛出函数参数抛出的任意错误。

例如,Swfit 6中的count(where:)方法接受一个闭包来执行匹配计算。这个闭包也可以抛出错误,这样count(where:)也会抛出同样的错误:

public func count<E>( where predicate: (Element) throws(E) -> Bool ) throws(E) -> Int {

当闭包不抛出错误的时候,throws(E)就是throws(Never)。也就意味着count(where:)不抛出错误。

虽然这个功能能吸引人,当时如果未来抛错可能会变化的时候,就不是一个好选择。特别是对于库中的代码,等于是锁死了你未来抛错的可能。

甚至来说Typed throws可能更加适用于对嵌入式Swift的场景,这里性能和可预测性非常重要。

参数包(Pack iteration)迭代

SE-0408引入了参数包的迭代,以此支持了Swift 5.9引入的参数包迭代loop的能力。

这也就是间接地让可以实现任意数量的元组比较。

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 }

简单来说,SE-0015只是简单支持了最多6个元素元组使用==比较,而SE-0408算是去除了这个限制。

非连续元素集合方法

SE-0270引入了新的集合方法,来支持更复杂的操作,例如移动删除不连续的多个元素。

这个改变是基于RangeSet的新类型来实行,和IndexSet很像,只是元素是任意Comparable元素,而不是只是简单的int。

很多Swift API也己经升级到了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%以上分数的indexRangeSet

let topResults = results.indices { student in student.score >= 85 }

然后如果你需要得到这些学生,可以使用新的Collection subscript:

for result in results[topResults] { print("\(result.student) scored \(result.score)%") }

上述会得到一个新的DiscontiguousSlice类型,和Slice很像性能的考量在新的集合中存放的是元素的引用,而DiscontiguousSlice中的坐标是不连续的。

RangeSetset的点在于,它支持来自于SetAlgebra协议的方法,例如union(), intersect()isSuperset(of:)。也因此将一个range插入另一个range,相同元素会merge而不是重复。

import 声明上的访问级修饰符

SE-0409增加了标注import声明访问级别的能力,例如可以标注为,private iimport SomeLibrary

这点很有用,例如库并不想对使用者暴漏内部包裹的依赖。标注internalprivate Swift就不会为其隐藏的库构建声明,除非我们特意声明为public

Swfit 6 默认处理为internal,以前的版本处理为public

不可复制类型更新

Swift 6升级了,Swift 5.9引入的不可复制类型

第一个升级是SE-427,一次引入了很多改进,最大的改进是每个struct,class,enum,泛型参数,protocol,Swift 6都自动遵循Copyable协议。除非你特意指定为~Copyable

这也就意味着,不可复制类型也可以遵循也标注了~Copyable的协议。(Copyable类型可以遵循不可复制协议`)。

SE-0429支持了不可复制类型局部消费的能力。

之前一个不可复制类型和另一个同样不可复制类型交互的时候很有问题,例如下面的很常规的代码在之前也是不合法的:

struct Package: ~Copyable { var from: String = "IMF" // Message也是~Copyable var message: Message consuming func read() { message.read() } }

现在合法了,当然前提是类型不能有deinit。

另一个改进是SE-0432,支持了在switch判断中借用不可复制类型。在之前的`where`闭包中是不可以的。

enum ImpossibleOrder: ~Copyable { case signed(Package) case anonymous(Message) }

里面的PackageMessage都是不可复制的,因此enumImpossibleOrder也得是不可复制的。

Swift 6之后,我们可以对实现switch的模式匹配了。

func issueOrders() { let message = Message(agent: "Ethan Hunt", message: "You need to abseil down a skyscraper for some reason.") let order = ImpossibleOrder.anonymous(message) switch consume order { case .signed(let package): package.read() case .anonymous(let message) where message.agent == "Ethan Hunt": print("Play dramatic music") message.read() case .anonymous(let message): message.read() } }

128b Int 类型

SE-0425引入了 Int128UInt128,这点无需多言。

let enoughForanybody: Int128 = 170_141_183_460_469_231_731_687_303_715_884_105_727

BitwiseCopyable

SE-0426引入了BitwiseCopyable协议,就是为了让所遵循类型能够到更优化的编译后代码。

大部分情况下你不需要手动来开启,Swift 会为 属性也都是按位拷贝的struct 和 enum自动开启,按位拷贝类型包括内置的整形,浮点数,Bool,Duration,StaticString等。

但是如果在库代码中,如果自动应用了BitwiseCopyable协议,在未来改变了类型,可能会导致问题,Swift基于这一点,在public导出或者package可见类型没有自动使用了BitwiseCopyable,除非这些类型标注为`@frozen`。

当然如果你可以使用~BitwiseCopyable来限制关闭。

@fronzen public enum CommandLine : ~BitsiseCopyable { }

上述虽然public也有@frozen,使用了~BitwiseCopyable,Swift,就会取消编译优化。

注:取消BitwiseCopyable需要直接在类型声明上使用~BitwiseCopyable,在扩展上使用不起作用。


整体翻译:https://www.hackingwithswift.com/articles/269/whats-new-in-swift-6