hiccLoghicc log by wccHipo日志

Swift 5.6到5.10新特性整理

toc

Intro

Swift 5.10

全局变量严格并发

SE-0412 进一步加强了 Swift 在编译时防止数据竞争的能力。

当你编写涉及共享状态的代码时,如果你不确保这个共享状态在跨线程使用时是安全的,你就会在许多地方遇到数据竞争的问题。

在 Swift 5.10 中,编译器只允许你在以下情况下从并发上下文访问共享的可变状态:

  • 这个状态是不可变的且符合 Sendable(在这里了解更多关于 Sendable 的信息)
  • 这个状态被隔离到一个全局 actor(如 @MainActor 或你自己编写的 actor)
var mutableGlobal = 1 // warning: var 'mutableGlobal' is not concurrency-safe because it is non-isolated global shared mutable state // (unless it is top-level code which implicitly isolates to @MainActor) final class NonsendableType { init() {} } struct S { static let immutableNonsendable = NonsendableType() // warning: static property 'immutableNonsendable' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor }

在任何其他情况下,编译器都会认为并发访问共享状态是不安全的。

如果你采取了一些措施来规避 Swift 并发的 actor 和 Sendability(例如,因为你正在处理使用 Semaphore或 DispatchQueue 来同步访问的遗留代码),你可以通过将全局变量标记为 nonisolated(unsafe) 来选择退出对它们的并发检查。这个标记将告诉编译器,它不需要对标记的属性进行任何安全检查;你已经确保了代码可以安全地在并发上下文中使用。

nonisolated(unsafe) var global: String

将属性标记为 nonisolated(unsafe) 很像强制解包一个属性。你可能确信你的代码是安全的,并且会按预期工作,但你是靠自己的。你已经告诉编译器,你知道你在做什么,你不需要编译器为你执行任何检查。

每当你想要使用 nonisolated(unsafe) 时,你都应该问自己,是否可以实际将你标记的类型隔离到一个全局 actor,或者你是否可以使属性的类型 Sendable 且不可变。

弃用 @UIApplicationMain@NSApplicationMain

SE-0383 @UIApplicationMain@NSApplicationMain 曾经分别是 iOS 和 macOS 应用程序声明合成的特定于平台的应用程序入口点的标准方式。自从 Swift 5.3 引入 @main 属性后,这些函数已经过时了。

这个版本将在 Swift 6 之前弃用这些替代的入口点属性,转而使用 @main,并且在 Swift 6 中使用它们会产生错误。

@UIApplicationMain // warning: '@UIApplicationMain' is deprecated in Swift 5 // fixit: Change `@UIApplicationMain` to `@main` final class MyApplication: UIResponder, UIApplicationDelegate { /**/ }

应该使用:

@main final class MyApplication: UIResponder, UIApplicationDelegate { /**/ }

允许在非泛型上下文中嵌套协议

之前协议根本不能嵌套,所以必须始终是模块中的顶级类型。SE-0404 Swift 5.10 将放宽这个限制。

例如,TableView.Delegate 自然是与表视图相关的委托协议。你应该将其声明为这样 - 嵌套在它们的 TableView 类中:

class TableView { protocol Delegate: AnyObject { func tableView(_: TableView, didSelectRowAtIndex: Int) } } class DelegateConformer: TableView.Delegate { func tableView(_: TableView, didSelectRowAtIndex: Int) { // ... } }

协议也可以嵌套在非泛型函数和闭包中。诚然,这的实用性有些有限,因为这些协议的所有一致性也必须在同一个函数中。

隔离的默认值表达式

SE-0411: 默认值表达式现在可以与封闭函数或相应的存储属性具有相同的隔离Isolate:

@MainActor func requiresMainActor() -> Int { ... } class C { @MainActor var x: Int = requiresMainActor() } @MainActor func defaultArg(value: Int = requiresMainActor()) { ... }

对于存储属性的隔离默认值,隐式初始化只在具有相同隔离的 init 主体中发生。这关闭了一个重要的数据竞争安全漏洞,即全局 actor 隔离的默认值可能会无意中从 actor 外部同步运行。

这个提案主要解决了以下问题:

  • 在并发上下文中,默认值表达式的隔离与其所属的函数或属性不一致,可能导致数据竞争。
  • 全局 actor 隔离的默认值可能会意外地从 actor 外部同步运行,违反了 actor 隔离的原则。

通过引入隔离的默认值表达式,提案提供了一种更安全、更一致的方式来处理并发环境下的默认值,减少了潜在的并发错误,提高了代码的可读性和可维护性。

更多:function parameter isolate

关于 Actor 和初始化

SE-0327 旨在加强 actor 的定义,明确 actor 实例的数据隔离何时开始和结束,以及在 actorinitdeinit 声明的主体内可以做什么。

actor 或全局 actor 隔离类型(GAIT)的非委托初始化器需要初始化该类型的所有存储属性。

虽然 actor 是引用类型,但它们的委托初始化器将遵循与值类型相同的基本规则,即:

  • 如果初始化器主体包含对某个 self.init 的调用,那么它就是一个委托初始化器。不需要使用 convenience 关键字。
  • 对于委托初始化器,在使用 self 之前,必须在所有路径上调用 self.init

actorclass 类型之间这种差异的原因是 actor 不支持继承,因此它们可以去掉类初始化器委托的复杂性。GAIT 使用与普通类相同的语法形式来定义委托初始化器。

initdeinit 之间的唯一区别是,deinit 只能访问 Sendable 属性,而 init 可以在隔离衰减之前访问非 Sendable 属性。

主要内容包括:

  1. Actor 的非委托初始化器(Non-delegating Initializers):
    • Actor 的非委托初始化器必须初始化该 Actor 的所有存储属性。
    • 这样可以确保 Actor 在初始化完成后,所有属性都已被正确初始化,避免并发访问未初始化的属性。
  2. Actor 的委托初始化器(Delegating Initializers):
    • 虽然 Actor 是引用类型,但其委托初始化器遵循与值类型相同的规则。
    • 如果初始化器主体包含对 self.init 的调用,那么它就是一个委托初始化器,不需要使用 convenience 关键字。
    • 对于委托初始化器,必须在所有路径上调用 self.init,然后才能使用 self。
  3. 全局 Actor 隔离类型(Global-Actor Isolated Types, GAIT):
    • GAIT 的初始化器规则与 Actor 相同。
    • GAIT 使用与普通类相同的语法形式来定义委托初始化器。
  4. Actor 的 deinit:
    • Actor 的 deinit 只能访问 Sendable 属性,而 init 可以在隔离衰减之前访问非 Sendable 属性。

这个提案的目标是通过明确 Actor 初始化器的行为和规则,确保 Actor 在并发环境下的正确性和安全性。它为 Actor 的初始化过程提供了清晰的指导方针,帮助开发者编写更加健壮和可靠的并发代码。

通过规范 Actor 初始化器的语义和约束,该提案旨在增强 Swift 并发模型的一致性和可预测性,提高开发者使用 Actor 的体验和效率。

Swift 5.9

ifswitch表达式

SE-0380 允许在 Swift 中将 if 和 switch 用作表达式,从而减少了代码中的样板代码。

主要优点:

  • **更简洁的代码:**不再需要在 if 和 switch 语句中使用 return 关键字。
  • 更具表现力:if 和 switch 表达式可以嵌套在其他表达式中,从而实现更灵活的代码结构。
// 在 Swift 5.1 之前 func rating(for score: Int) -> String { if score > 500 { return "Pass" } else { return "Fail" } } // 在 Swift 5.1 及更高版本中 func rating(for score: Int) -> String { score > 500 ? "Pass" : "Fail" }

注意事项:

  • if 和 switch 表达式中的不同分支必须具有相同的类型。
  • if 表达式中的条件必须是布尔值。

值和类型参数包

参数包允许你编写处理任意数量类型的泛型类型和函数。

例如,如果没有参数包,如果你想编写一个名为 all 的函数来检查任意数量的 Optional 值是否为 nil,你需要为每个你想要支持的参数长度编写一个单独的重载,从而创建一个任意的上限:

func all<W1>(_ optional: W1?) -> W1? func all<W1, W2>(_ optional1: W1?, optional2: W2?) -> (W1, W2)? func all<W1, W2, W3>(_ optional1: W1?, optional2: W2?, optional3: W3?) -> (W1, W2, W3)?

使用参数包,你可以将此 API 表示为一个没有上限的单个函数,允许你传递任意数量的参数:

func all<each Wrapped>(_ optional: repeat (each Wrapped)?) -> (repeat each Wrapped)?

调用使用参数包的 API 很直观,不需要额外的工作:

if let (int, double, string, bool) = all(optionalInt, optionalDouble, optionalString, optionalBool) { print(int, double, string, bool) } else { print("got a nil") }

多参数写法也是支持的:

func pairUp3<each T: WritesFrontEndCode, each U: WritesBackEndCode>(firstPeople: repeat each T, secondPeople: repeat each U) -> (repeat (first: each T, second: each U)) { return (repeat (each firstPeople, each secondPeople)) }

SE-0382SE-0389SE-0397 为 Swift 带来了宏。宏是一种强大的工具,允许你创建在编译时转换代码的代码。

关键要点:

  • 宏是类型安全的,需要确切地知道它们将使用什么数据。
  • 它们作为外部程序在构建阶段运行。
  • 宏有各种类型,包括用于生成表达式的 ExpressionMacro 和用于添加 getter 和 setter 的 AccessorMacroConformanceMacro 用于使类型符合协议。
  • 宏与你的源代码一起工作,允许你查询和操作代码的各个部分。
  • 它们在沙盒中工作,只能操作给定的数据。
  • Swift 的宏支持是围绕 Apple 的 SwiftSyntax 库构建的,用于理解和操作源代码。你必须将此作为宏的依赖项添加

使用宏的步骤:

  1. 创建一个执行宏展开的代码。
  2. 在一个单独的模块中创建一个符合 CompilerPlugin 协议的结构,导出你的宏。
  3. 在你的 Package.swift 文件中添加宏模块。
  4. 在你的主目标中声明宏。
  5. 使用宏。

使用宏:

  • 允许创建复杂且动态的代码转换。
  • 可以提高开发效率,因为你可以避免编写重复或复杂的手动代码。

但是也会:

  • 宏可能很复杂且难以调试。
  • 它们可能会使代码更难理解和维护。

例如 首先,我们需要创建执行宏展开的代码——将 #buildDate 变成类似 2023-06-05T18:00:00Z 的东西:

public struct BuildDateMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) -> ExprSyntax { let date = ISO8601DateFormatter().string(from: .now) return "\"\(raw: date)\"" } }

重要提示: 此代码不应位于你的主应用程序目标中;我们不希望该代码被编译到我们的最终应用程序中,我们只希望其中包含最终的日期字符串。 在同一个模块中,我们创建一个符合 CompilerPlugin 协议的结构,导出我们的宏:

import SwiftCompilerPlugin import SwiftSyntaxMacros @main struct MyMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ BuildDateMacro.self ] }

然后,我们将其添加到 Package.swift 中的目标列表中:

.macro( name: "MyMacrosPlugin", dependencies: [ .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ] ),

这就完成了在外部模块中创建宏。我们代码的其余部分发生在我们想使用宏的任何地方,例如在我们的主应用程序目标中。 这需要两个步骤,首先定义宏是什么。在我们的例子中,这是一个将返回一个字符串的独立表达式宏,它存在于 MyMacrosPlugin 模块中,并且具有严格的名称 BuildDateMacro。因此,我们将此定义添加到我们的主目标:

@freestanding(expression) macro buildDate() -> String = #externalMacro(module: "MyMacrosPlugin", type: "BuildDateMacro")

第二个步骤是实际使用宏,如下所示:

print(#buildDate)

当你阅读这段代码时,最重要的收获是,主要的宏功能——BuildDateMacro 结构中的所有代码——在构建时运行,其结果被注入到调用站点。因此,我们上面的 print() 调用将被重写为类似这样的内容:

print("2023-06-05T18:00:00Z")

再例如,尝试一个更有用的宏,这次制作一个成员属性宏。当应用于类型(例如类)时,这允许我们对类中的每个成员应用一个属性。这与较旧的 @objcMembers 属性在概念上是相同的,它将 @objc 添加到类型中的每个属性。 例如,如果你有一个使用 @Published 在其每个属性上可观察对象,你可以编写一个简单的 @AllPublished 宏来为你完成这项工作。首先,编写宏本身:

public struct AllPublishedMacro: MemberAttributeMacro { public static func expansion( of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingAttributesFor member: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [AttributeSyntax] { [AttributeSyntax(attributeName: SimpleTypeIdentifierSyntax(name: .identifier("Published")))] } }

其次,将其包含在你的提供宏列表中:

struct MyMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ BuildDateMacro.self, AllPublishedMacro.self, ] }

第三,在你的主应用程序目标中声明宏,这次将其标记为附加成员属性宏:

@attached(memberAttribute) macro AllPublished() = #externalMacro(module: "MyMacrosPlugin", type: "AllPublishedMacro")

现在使用它来注释你的可观察对象类:

@AllPublished class User: ObservableObject { var username = "Taylor" var age = 26 }

我们的宏能够接受参数来控制它们的行为,不过复杂性就容易急剧上升。

Swift 团队成员维护了一个Github,里面有些 macro 的示例

不可复制的结构体struct 和枚举 enum

SE-0390 引入了不可复制的结构体和枚举。相反可复制的结构体和枚举的单个实例可以在多个地方共享——虽然在代码各个地方访问,但最终仍然只有一个所有者。

首先,这里引入了一个新语法:~Copyable。这个语法是一体的,目前没有其他的~Equatable像的语法。

因此我们创建一个不可复制User结构体:

struct User: ~Copyable { var name: String }

注意:不可复制类型,不能再遵循Sendable之外的协议。

一旦我们声明为不可复制,行为就和以前不一样了,例如下面

func createUser() { let newUser = User(name: "Anonymous") var userCopy = newUser print(userCopy.name) } createUser()

上面的使用方式看起来没什么区别,但是我们已经将 User 结构体声明为不可复制的 - 它怎么能复制 newUser 呢?答案是它不能:将 newUser 赋值给 userCopy 会导致原始的 newUser 值被消耗(consume),这意味着它不能再被使用,因为所有权现在属于 userCopy。如果你尝试将 print(userCopy.name) 更改为 print(newUser.name),你会看到 Swift 抛出编译器错误 - 这是不允许的。

~Copyable实例consume 之后,调用原值报错

SE-0377 将不可复制类型作为函数参数时也有新的限制:

  • 如果准备消耗标注 consuming。意味着函数调用后,原值就会无效。
  • 或者标注为 borrowing,和其他借用者一起读取该值,如下。
func createAndGreetUser() { let newUser = User(name: "Anonymous") greet(newUser) print("Goodbye, \(newUser.name)") } func greet(_ user: borrowing User) { print("Hello, \(user.name)!") } createAndGreetUser()

如果我们让 greet() 函数使用 consuming User,则不允许 print("Goodbye, (newUser.name)") - Swift 会认为在 greet() 运行后 newUser 值无效。另一方面,因为消耗方法必须结束对象的生命周期,所以它们可以自由地改变其属性。

consuming 不可复制实例后使用报错

这种共享行为赋予了不可复制的结构体一种超能力,而这种超能力以前仅限于类和参与者:我们可以为它们提供析构器,当对不可复制实例的最后一个引用被销毁时,它将自动运行。

重要说明: 这与类上的析构器的行为略有不同,这可能是早期实现的小问题或有意的行为。

首先,这是一些使用类的析构器的代码:

class Movie { var name: String init(name: String) { self.name = name } deinit { print("\(name) is no longer available") } } func watchMovie() { let movie = Movie(name: "The Hunt for Red October") print("Watching \(movie.name)") } watchMovie()

当它运行时,它会打印"Watching The Hunt for Red October",然后打印"The Hunt for Red October is no longer available"。但如果你将类型的定义从 class Movie 更改为 struct Movie: ~Copyable,你会看到这两个 print() 语句以相反的顺序运行。

不可复制类型内的方法默认是借用的,但它们可以像可复制类型一样标记为mutating可变的,并且它们也可以标记为消耗性的,表示该值在方法运行后无效。

struct MissionImpossibleMessage: ~Copyable { private var message: String init(message: String) { self.message = message } consuming func read() { print(message) } }

这将消息本身标记为私有,因此只能通过调用消耗实例的 read() 方法来访问它。

与可变方法不同,消耗性方法可以在类型的常量let实例上运行。因此,像这样的代码是可以的:

func createMessage() { let message = MissionImpossibleMessage(message: "You need to abseil down a skyscraper for some reason.") message.read() } createMessage()

注意: 因为 message.read() 消耗了消息实例,所以尝试再次调用 message.read() 是错误的。

当与析构器结合使用时,消耗性方法会变得更加复杂,因为它们可能会重复执行你所做的任何清理工作。例如,如果你在游戏中跟踪高分,你可能希望有一个消耗性的 finalize() 方法,将最新的高分写入永久存储并阻止其他人进一步更改分数,但你也可能有一个析构器,在对象被销毁时将最新的分数保存到磁盘。

为了避免这个问题,Swift 5.9 引入了一个新的 discard 运算符,可用于不可复制类型的消耗性方法。当你在消耗性方法中使用 discard self 时,它会阻止为此对象运行析构器。

因此,我们可以像这样实现我们的 HighScore 结构体:

struct HighScore: ~Copyable { var value = 0 consuming func finalize() { print("Saving score to disk…") discard self } deinit { print("Deinit is saving score to disk…") } } func createHighScore() { var highScore = HighScore() highScore.value = 20 highScore.finalize() } createHighScore()

提示:当该代码运行时,你会看到析构器消息被打印两次 - 一次是当我们更改 value 属性时,这实际上销毁并重新创建了结构体,另一次是在 createHighScore() 方法结束时。

在使用这个新功能时,你需要注意一些额外的复杂性:

  1. 类和 actor 不能是不可复制的。
  2. 不可复制类型目前不支持泛型,这排除了可选的不可复制对象以及不可复制对象的数组。
  3. 如果你在另一个结构体或枚举中使用不可复制类型作为属性,则该父结构体或枚举也必须是不可复制的。
  4. 你需要非常小心地从现有类型中添加或删除 ~Copyable,因为它会显著改变它们的使用方式。如果你在库中发布代码,这会破坏你的 ABI稳定。

使用consume运算符来终结变量绑定的生命周期

SE-0366consume扩展到可复制类型的局部变量和常量,这可能有利于那些希望避免在数据传递过程中幕后发生过多 retain/release 调用的开发者。

consume 运算符看起来像这样:

struct User { var name: String } func createUser() { let newUser = User(name: "Anonymous") let userCopy = consume newUser print(userCopy.name) } createUser()

其中重要的一行是 let userCopy 行,它同时做了两件事:

  1. 将值从 newUser 复制到 userCopy
  2. 终结 newUser 的生命周期,因此任何进一步尝试访问它都会抛出错误。

这允许我们明确地告诉编译器"不要允许我再次使用这个值"。

更常见的做法是,使用_将其投入 black hole——不想复制数据,只是想将其标记为已销毁。类似:

func consumeUser() { let newUser = User(name: "Anonymous") _ = consume newUser }

或者给函数传值时候使用:

func createAndProcessUser() { let newUser = User(name: "Anonymous") process(user: consume newUser) } func process(user: User) { print("Processing \(name)…") } createAndProcessUser()

此特性两个额外的点特别值得了解。

首先,Swift 跟踪代码中哪些分支消耗了值,并有条件地执行规则。因此,在这段代码中,只有两种可能性中的一种消耗了我们的 User 实例:

func greetRandomly() { let user = User(name: "Taylor Swift") if Bool.random() { let userCopy = consume user print("Hello, \(userCopy.name)") } else { print("Greetings, \(user.name)") } } greetRandomly()

其次,从技术上讲,consume 操作的是绑定而不是值。实际上,这意味着如果我们使用变量进行消耗,我们可以重新初始化该变量并正常使用它:

func createThenRecreate() { var user = User(name: "Roy Kent") _ = consume user user = User(name: "Jamie Tartt") print(user.name) } createThenRecreate()

AsyncStreamAsyncThrowingStream 的便捷 makeStream 方法

SE-0388AsyncStreamAsyncThrowingStream 添加了一个新的 makeStream() 方法,该方法同时返回流本身及其延续(continuation)。

因此,我们不再需要编写如下代码:

var continuation: AsyncStream<String>.Continuation! let stream = AsyncStream<String> { continuation = $0 }

现在我们可以同时获得两者:

let (stream, continuation) = AsyncStream.makeStream(of: String.self)

这在需要在当前上下文之外访问延续的地方特别受欢迎,例如在不同的方法中。例如,以前我们可能会编写一个简单的数字生成器,像这样,它需要将延续存储为自己的属性,以便能够从 queueWork() 方法中调用它:

struct OldNumberGenerator { private var continuation: AsyncStream<Int>.Continuation! var stream: AsyncStream<Int>! init() { stream = AsyncStream(Int.self) { continuation in self.continuation = continuation } } func queueWork() { Task { for i in 1...10 { try await Task.sleep(for: .seconds(1)) continuation.yield(i) } continuation.finish() } } }

使用新的 makeStream(of:) 方法,这段代码变得更加简单:

struct NewNumberGenerator { let (stream, continuation) = AsyncStream.makeStream(of: Int.self) func queueWork() { Task { for i in 1...10 { try await Task.sleep(for: .seconds(1)) continuation.yield(i) } continuation.finish() } } }

Clock 添加 sleep(for:) 方法

SE-0374 为 Swift 的 Clock 协议添加了一个新的扩展方法,允许我们将执行暂停指定的秒数,并且还扩展了基于持续时间的 Task 睡眠以支持特定的容差。

Clock 的改变虽小但很重要,特别是如果你在测试中模拟一个具体的 Clock 实例以消除生产环境中原本存在的延迟。

例如,这个类可以用任何类型的 Clock 创建,并在触发保存操作之前使用该 Clock 进行睡眠:

class DataController: ObservableObject { var clock: any Clock<Duration> init(clock: any Clock<Duration>) { self.clock = clock } func delayedSave() async throws { try await clock.sleep(for: .seconds(1)) print("Saving…") } }

因为它使用了 any Clock<Duration>,所以现在可以在生产环境中使用像 ContinuousClock 这样的东西,但在测试中使用你自己的 DummyClock,在那里你可以忽略所有的sleep() 命令以保持测试快速运行。

在旧版本的 Swift 中,理论上等效的代码是 try await clock.sleep(until: clock.now.advanced(by: .seconds(1))),但在这个例子中这不起作用,因为 clock.now 不可用,因为 Swift 不知道具体使用了什么类型的 Clock。

至于对 Task 睡眠的改变,它意味着我们可以从这样的代码:

try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5))

简化为:

try await Task.sleep(for: .seconds(1), tolerance: .seconds(0.5))

丢弃任务组

SE-0381 添加了新的可丢弃任务组,修复了当前 API 中的一个重要缺陷:在任务组内创建的任务一旦完成就会自动丢弃和销毁,这意味着长时间运行(或者像 Web 服务器那样可能永远运行)的任务组不会随时间泄漏内存。

使用原始的 withTaskGroup() API 时,可能会出现一个问题,因为 Swift 只在我们调用 next() 或遍历任务组的子任务时才丢弃子任务及其结果数据。如果所有子任务当前都在执行,调用 next() 会导致代码暂停,所以我们遇到了问题:你希望服务器始终监听连接,以便可以添加任务来处理它们,但你也需要偶尔停下来清理已完成的旧任务。

在 Swift 5.9 之前,这个问题没有干净的解决方案。Swift 5.9 添加了 withDiscardingTaskGroup()withThrowingDiscardingTaskGroup() 函数,它们创建新的丢弃任务组。这些任务组会在每个任务完成后自动丢弃和销毁它,而无需我们手动调用 next() 来消费它。

为了让你了解触发此问题的原因,我们可以实现一个简单的目录监视器,它永远循环并报告添加或删除的任何文件或目录的名称:

struct FileWatcher { // The URL we're watching for file changes. let url: URL // The set of URLs we've already returned. private var handled = Set<URL>() init(url: URL) { self.url = url } mutating func next() async throws -> URL? { while true { // Read the latest contents of our directory, or exit if a problem occurred. guard let contents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else { return nil } // Figure out which URLs we haven't already handled. let unhandled = handled.symmetricDifference(contents) if let newURL = unhandled.first { // If we already handled this URL then it must be deleted. if handled.contains(newURL) { handled.remove(newURL) } else { // Otherwise this URL is new, so mark it as handled. handled.insert(newURL) return newURL } } else { // No file difference; sleep for a few seconds then try again. try await Task.sleep(for: .microseconds(1000)) } } } }

然后我们可以在一个简单的应用程序中使用它,为了简洁起见,我们只打印 URL 而不做任何实际的复杂处理:

struct FileProcessor { static func main() async throws { var watcher = FileWatcher(url: URL(filePath: "/Users/twostraws")) try await withThrowingTaskGroup(of: Void.self) { group in while let newURL = try await watcher.next() { group.addTask { process(newURL) } } } } static func process(_ url: URL) { print("Processing \(url.path())") } }

这将永远运行,或者至少直到用户终止程序或我们监视的目录不再可访问为止。但是,因为它使用了 withThrowingTaskGroup(),所以有一个问题:每次调用 addTask() 时都会创建一个新的子任务,但因为它不在任何地方调用 group.next(),所以这些子任务永远不会被销毁。这段代码将一点一点地(每次可能只有几百个字节)吃掉越来越多的内存,直到最终操作系统的 RAM 耗尽,被迫终止程序。

使用丢弃任务组可以完全解决这个问题:只需将 withThrowingTaskGroup(of: Void.self) 替换为 withThrowingDiscardingTaskGroup,每个子任务就会在其工作完成后自动销毁。

实际上,这个问题主要会出现在服务器代码中,服务器必须能够接受新连接,同时平稳地处理现有连接。

Swift 5.8

去除result builder中变量的限制

SE-0373 放宽了在结果构建器中使用变量时的一些限制,允许我们编写以前会被编译器禁止的代码。

例如,在 Swift 5.8 中,我们可以在结果构建器中直接使用惰性变量,如下所示:

struct ContentView: View { var body: some View { VStack { lazy var user = fetchUsername() Text("Hello, \(user).") } .padding() } func fetchUsername() -> String { "@twostraws" } }

这展示了这个概念,但并没有提供任何好处,因为惰性变量总是被使用——在该代码中使用 lazy varlet 没有区别。要了解它实际有用的地方需要一个更长的代码示例,如下所示:

// 用户是活跃订阅者、非活跃订阅者,或者我们还不知道他们的状态。 enum UserState { case subscriber, nonsubscriber, unknown } // 关于用户的两条小信息 struct User { var id: UUID var username: String } struct ContentView: View { @State private var state = UserState.unknown var body: some View { VStack { lazy var user = fetchUsername() switch state { case .subscriber: Text("Hello, \(user.username). Here's what's new for subscribers…") case .nonsubscriber: Text("Hello, \(user.username). Here's why you should subscribe…") Button("Subscribe now") { startSubscription(for: user) } case .unknown: Text("Sign up today!") } } .padding() } // 示例函数,将执行复杂的工作 func fetchUsername() -> User { User(id: UUID(), username: "Anonymous") } func startSubscription(for user: User) { print("Starting subscription…") } }

这种方法解决了在备选方案中会出现的问题:

  • 如果我们不使用 lazy,那么 fetchUsername() 将在 state 的所有三种情况下都被调用,即使在一种情况下它没有被使用。
  • 如果我们删除 lazy 并将对 fetchUsername() 的调用放在两个 case 中,那么我们将复制代码——对于简单的单行代码来说这不是一个大问题,但你可以想象这将在更复杂的代码中如何引起问题。
  • 如果我们将 user 移到计算属性中,那么当用户点击“立即订阅”按钮时,它将被再次调用。

此更改还允许我们在结果构建器中使用属性包装器和局部计算属性,尽管我怀疑它们不太有用。例如,现在允许以下类型的代码:

struct ContentView: View { var body: some View { @AppStorage("counter") var tapCount = 0 Button("Count: \(tapCount)") { tapCount += 1 } } }

但是,尽管这会导致底层的 UserDefaults 值随着每次点击而改变,但以这种方式使用 @AppStorage 不会导致每次 tapCount 更改时 body 属性被重新调用——我们的 UI 不会自动更新以反映更改。自动更新以反映更改。

函数后向部署

SE-0376 添加了一个新的 @backDeployed 属性,它允许在新版本的框架中使用新 API。它的工作原理是将函数的代码写入你的应用程序二进制文件中,然后执行运行时检查:如果你的用户使用的是足够新的操作系统版本,那么将使用系统自己的函数版本,否则将使用复制到你的应用程序二进制文件中的版本。

从表面上看,这听起来像是 Apple 通过追溯的方式在早期操作系统中提供一些新功能的好方法,但我认为这并不是什么灵丹妙药——@backDeployed 仅适用于函数、方法、下标和计算属性,因此虽然它可能非常适合较小的 API 更改,例如 iOS 16.1 中引入的 fontDesign() 修饰符,但它不适用于需要使用新类型的任何代码,例如依赖于新的 ScrollBounceBehavior 结构的新 scrollBounceBehavior() 修饰符。

例如,iOS 16.4 为 Text 引入了 monospaced(_ isActive:) 。如果这使用 `@backDeployed`,SwiftUI 团队可能会确保该修饰符可用于支持他们实际需要的实现代码的最早 SwiftUI 版本,如下所示:

extension Text { @backDeployed(before: iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4) @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) public func monospaced(_ isActive: Bool) -> Text { fatalError("Implementation here") } }

如果像这样实现修饰符,在运行时 Swift 将使用 SwiftUI 的系统副本(如果它已经具有该修饰符),否则使用回溯部署的版本回到 iOS 14.0 和类似版本。实际上,尽管这不会公开任何新类型,因此看起来是回溯部署的简单选择,但我们不知道 SwiftUI 在内部使用什么类型,因此很难预测哪些可以回溯部署,哪些不能回溯部署。 在内部使用什么类型,因此很难预测什么可以后向部署,什么不可以。

weak self解开后,支持隐式self

SE-0365 通过允许在弱 self 捕获被解包的地方使用隐式 self向让我们从闭包中移除 `self。

class TimerController { var timer: Timer? var fireCount = 0 init() { timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in guard let self else { return } print("Timer has fired \(fireCount) times") fireCount += 1 } } }

在Swift 5.8 以前的版本,必须使用self.fireCount

简化文件魔法标识符

SE-0274简化了 #file魔法标识符来使用Module/Filename,例如MyApp/ContentView.swift。以前#file包含完整的路径,如果/Users/xxx/Desktop/yyy/ContentView.swift。 很长,而且包含了一些隐私信息。

如果你想用老的全路径,可以使用#filePath

// New behavior, when enabled print(#file) // Old behavior, when needed print(#filePath)

注意:目前这个特性默认没有打开,SE-0362加了个一个-enable-upcoming-feature的编译标志来打开新特性,如果想要这个特性,可以使用-enable-upcoming-feature ConciseMagicFile

存在类参数可以可选使用

SE-0375扩展了 Swift 5.7 的一项功能,该功能允许我们使用协议调用泛型函数,从而修复了一个小但令人讨厌的不一致之处:Swift 5.7 不允许对可选类型使用此行为,而 Swift 5.8 则允许。

下面的非可选`T` 函数在Swift 5.7中可以正常使用。

func double<T: Numeric>(_ number: T) -> T { number * 2 } let first = 1 let second = 2.0 let third: Float = 3 let numbers: [any Numeric] = [first, second, third] for number in numbers { print(double(number)) }

Swift 5.8中,可选参数也可以使用了。

func optionalDouble<T: Numeric>(_ number: T?) -> T { let numberToDouble = number ?? 0 return numberToDouble * 2 } let first = 1 let second = 2.0 let third: Float = 3 let numbers: [any Numeric] = [first, second, third] for number in numbers { print(optionalDouble(number)) }

上面代码,swift 5.7会报错“Type 'any Numeric' cannot conform to 'Numeric’”。

集合类型支持强制向下转化

Swift 5.8解决了之前在某些情况下不允许对集合进行强制转换——例如将 ClassA 数组强制转换为继承自` ClassA` 的另一种类型的数组。

class Pet { } class Dog: Pet { func bark() { print("Woof!") } } func bark(using pets: [Pet]) { switch pets { case let pets as [Dog]: for pet in pets { pet.bark() } default: print("No barking today.") } }

在之前,这里会报错“Collection downcast in cast pattern is not implemented; use an explicit downcast to '[Dog]' instead.”。 需要使用if let dogs = pets as? [Dog] { 的语法才行。

Swift 5.7

if let快捷解可选包

SE-0345 引入了一种新的简写语法,用于使用 if letguard let 将可选值解包到具有相同名称的阴影变量中。这意味着我们现在可以编写这样的代码:

var name: String? = "Linda" if let name { print("Hello, \(name)!") }

此更改不适用于对象内的属性,这意味着像这样的代码将无法工作:

struct User { var name: String } let user: User? = User(name: "Linda") // 无法工作 if let user.name { print("Welcome, \(user.name)!") }

闭包类型推导增强

SE-0326 极大地提高了 Swift 在闭包中使用参数和类型推断的能力,使得在许多情况下,我们无需明确指定输入和输出类型。这使得代码更简洁,更容易阅读。

Swift 5.7 可以这样写,不会报错:

let scores = [100, 80, 85] let results = scores.map { score in if score >= 85 { return "\(score)%: Pass" } else { return "\(score)%: Fail" } }

以前版本,需要明确写明返回类型:

let oldResults = scores.map { score -> String in if score >= 85 { return "\(score)%: Pass" } else { return "\(score)%: Fail" } }

Clock, Instant, Duration

SE-0329 引入了一种新的、标准化的方式来引用 Swift 中的时间和持续时间。正如其名称所示,它分为三个主要组成部分:

  • Clock 表示一种衡量时间流逝的方式。内置了两种:连续时钟即使在系统休眠时也会持续计时,而暂停时钟则不会。
  • Instant 表示一个确切的时间点。
  • Duration 持续时间表示两个 Instant 之间经过的时间。

对于许多人来说,最直接的应用将是新升级的 Task API,它现在可以以比纳秒更合理的方式指定睡眠时间:

try await Task.sleep(until: .now + .seconds(1), clock: .continuous)

这个新的 API 还带来了能够指定容忍度的好处,这允许系统在睡眠截止日期之后稍微等待一下,以最大化电源效率。所以,如果我们想要至少睡眠 1 秒,但是希望总共持续到 1.5 秒,我们会这样写:

try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5), clock: .continuous)

提示:这个容忍度只是在默认睡眠时间的基础上增加的-系统在至少过去 1 秒后才会结束睡眠。

尽管这还没有发生,但看起来旧的基于纳秒的 API 将在不久的将来被弃用。

时钟也有助于测量一些特定的工作,这对于你想向用户展示类似于文件导出花费了多长时间这样的事情非常有帮助:

let clock = ContinuousClock() let time = clock.measure { // complex work here } print("Took \(time.components.seconds) seconds")

正则表达式大改进

Swift 5.7 引入了一系列与正则表达式相关的改进,极大地改进了我们处理字符串的方式。

  • SE-0350 引入了一个新的 Regex 类型
  • SE-0351 引入了一个由结果构建器驱动的 DSL,用于创建正则表达式。
  • SE-0354 添加了使用 /.../ 而不是通过 Regex 和字符串创建正则表达式的能力。
  • SE-0357 添加了许多基于正则表达式的新字符串处理算法。

首先,我们有了一些新的字符串处理方法:

let message = "the cat sat on the mat" print(message.ranges(of: "at")) print(message.replacing("cat", with: "dog")) print(message.trimmingPrefix("the "))

强大的是,我们可以使用正则表达式:

print(message.ranges(of: /[a-z]at/)) print(message.replacing(/[a-m]at/, with: "dog")) print(message.trimmingPrefix(/The/.ignoresCase()))

除了 regex 字面量,Swift 还提供了一个专门的 Regex 类型。

do { let atSearch = try Regex("[a-z]at") print(message.ranges(of: atSearch)) } catch { print("Failed to create regex") }

有一个关键的区别:

  • 当我们使用 Regex 从字符串创建正则表达式时,Swift 必须在运行时解析字符串以确定它应该使用的实际表达式。
  • 相比之下,使用 regex 字面量允许 Swift 在编译时检查你的 regex:它可以验证 regex 不包含错误,并且也能理解它将包含的确切匹配项。

因此我们可以如下使用,字面量的方式。

let search1 = /My name is (.+?) and I'm (\d+) years old./ let greeting1 = "My name is Taylor and I'm 26 years old." if let result = try? search1.wholeMatch(in: greeting1) { print("Name: \(result.1)") print("Age: \(result.2)") }

注意,result的 tuple,可以通过.1.2 的方式来引用我们的匹配(.0表示整个匹配的字符串)

甚至我们还可以给匹配命名:

let search2 = /My name is (?<name>.+?) and I'm (?<age>\d+) years old./ let greeting2 = "My name is Taylor and I'm 26 years old." if let result = try? search2.wholeMatch(in: greeting2) { print("Name: \(result.name)") print("Age: \(result.age)") }

Swift 中甚至还可以通过一种像 SwiftUI 的 DSL 的方式创建Regex。

import RegexBuilder let search3 = Regex { "My name is " Capture { OneOrMore(.word) } " and I'm " Capture { OneOrMore(.digit) } " years old." }

设置,可是使用TryCapture而不是Capture,配合转化方法

let search4 = Regex { "My name is " Capture { OneOrMore(.word) } " and I'm " TryCapture { OneOrMore(.digit) } transform: { match in Int(match) } " years old." }

甚至匹配命名也可以做到:

let nameRef = Reference(Substring.self) let ageRef = Reference(Int.self) let search5 = Regex { "My name is " Capture(as: nameRef) { OneOrMore(.word) } " and I'm " TryCapture(as: ageRef) { OneOrMore(.digit) } transform: { match in Int(match) } " years old." } if let result = greeting1.firstMatch(of: search5) { print("Name: \(result[nameRef])") print("Age: \(result[ageRef])") }

默认值的类型推导

SE-0347 支持了,我们在函数泛型参数中使用默认值。

func drawLotto2<T: Sequence>(from options: T = 1...49, count: Int = 7) -> [T.Element] { Array(options.shuffled().prefix(count)) }

顶层代码支持并发Concurrency代码

SE-0343 能让我们在顶层代码中使用 concurrency 代码。

import Foundation let url = URL(string: "https://hws.dev/readings.json")! let (data, _) = try await URLSession.shared.data(from: url) let readings = try JSONDecoder().decode([Double].self, from: data) print("Found \(readings.count) temperature readings")

参数类型支持不透明some声明

SE-0341 解锁了在参数声明中使用 some 的能力,可以替换某些简单的泛型的地方。

func isSorted(array: [some Comparable]) -> Bool { array == array.sorted() }

[some Comparable] 参数类型意味着此函数适用于包含符合 Comparable 协议的一种类型的元素的数组,这是等效泛型代码的语法糖:

func isSortedOld<T: Comparable>(array: [T]) -> Bool { array == array.sorted() }

当然,我们任然可以编写更长的受限扩展:

extension Array where Element: Comparable { func isSorted() -> Bool { self == self.sorted() } }

这种简化的泛型语法意味着我们不再能为类型添加更复杂的约束,因为对于合成的泛型参数没有特定的名称。

支持更多形式的不透明some类型

SE-0328 扩大了可以使用不透明结果类型的范围。

例如,我们现在可以一次返回多个不透明类型:

import SwiftUI func showUserDetails() -> (some Equatable, some Equatable) { (Text("Username"), Text("@twostraws")) }

我们还可以返回不透明类型:

func createUser() -> [some View] { let usernames = ["@frankefoster", "@mikaela__caron", "@museumshuffle"] return usernames.map(Text.init) }

甚至可以返回一个函数,当调用该函数时,它本身会返回一个不透明类型:

func createDiceRoll() -> () -> some View { return { let diceRoll = Int.random(in: 1...6) return Text(String(diceRoll)) } }

所有协议protocol都可以作为存在类型Existential Type

SE-0309 极大地放宽了 Swift 在协议具有 Self 或关联类型要求时禁止使用协议作为类型的限制,转向一个仅基于它们所做的特定属性或方法受限的模型。

简单来说,这意味着以下代码变得合法:

let firstName: any Equatable = "Paul" let lastName: any Equatable = "Hudson"

Equatable 是一个带有 Self 要求的协议,这意味着它提供了引用采用它的特定类型的功能。例如,Int 符合 Equatable,所以当我们说 4 == 4 时,我们实际上是在运行一个接受两个整数并在它们匹配时返回 true 的函数。

Swift 可以使用类似于 func ==(first: Int, second: Int) -> Bool 的函数来实现这个功能,但这不会很好地扩展 - 他们需要编写几十个这样的函数来处理布尔值、字符串、数组等。因此,Equatable 协议有一个类似的要求:func ==(lhs: Self, rhs: Self) -> Bool。用英语来说,这意味着“你需要能够接受两个相同类型的实例,并告诉我它们是否相同。”这可能是两个整数、两个字符串、两个布尔值,或者是符合 Equatable 的任何其他类型的两个实例。

为了避免这个问题和类似的问题,任何时候 Self 出现在 Swift 5.7 之前的协议中,编译器都不允许我们在代码中使用它,例如:

let tvShow: [any Equatable] = ["Brooklyn", 99]

从 Swift 5.7 开始,这段代码是允许的,现在限制被推迟到你尝试在 Swift 必须实际执行其限制的地方使用类型的情况。这意味着我们不能编写 firstName == lastName,因为正如我所说,== 必须确保它有两个相同类型的实例才能工作,而使用 any Equatable 我们隐藏了数据的确切类型。

然而,我们获得的是能够对数据进行运行时检查,以确定我们正在处理的具体内容。在我们的混合数组的情况下,我们可以编写这样的代码:

for item in tvShow { if let item = item as? String { print("Found string: \(item)") } else if let item = item as? Int { print("Found integer: \(item)") } }

或者在我们的两个字符串的情况下,我们可以使用这个:

if let firstName = firstName as? String, let lastName = lastName as? String { print(firstName == lastName) }

理解这个更改的关键是记住它允许我们更自由地使用这些协议,只要我们不做任何特别需要了解类型内部的事情。因此,我们可以编写代码来检查任何序列中的所有项目是否符合 Identifiable 协议:

func canBeIdentified(_ input: any Sequence) -> Bool { input.allSatisfy { $0 is any Identifiable } }

简而言之,SE-0309 放宽了 Swift 对于具有 Self 或关联类型要求的协议作为类型使用的限制。这使得我们可以更自由地使用这些协议,只要我们不进行任何特定需要了解类型内部的操作。这样,我们可以编写更灵活的代码,同时保持类型安全。

简化同类主关联类型

SE-0346 为引用具有特定关联类型的协议添加了新的、更简单的语法。

例如,如果我们编写的代码是以不同的方式缓存不同类型的数据,我们可能会这样开始:

protocol Cache<Content> { associatedtype Content var items: [Content] { get set } init(items: [Content]) mutating func add(item: Content) }

注意,协议现在看起来既像协议又像泛型类型 - 它有一个关联类型,声明了符合类型必须填充的某种空洞,但也在尖括号中列出了该类型:Cache<Content>

尖括号中的部分是 Swift 称为其主要关联类型的部分,重要的是要理解并不是所有的关联类型都应该在那里声明。相反,你应该只列出调用代码通常特别关心的那些,例如字典键和值的类型或 Identifiable 协议中的标识符类型。在我们的例子中,我们说我们的缓存的内容 - 字符串、图像、用户等 - 是其主要关联类型。

此时,我们可以像以前一样继续使用我们的协议 - 我们可能会创建一些我们想要缓存的数据,然后创建一个符合协议的具体缓存类型,如下所示:

struct File { let name: String } struct LocalFileCache: Cache { var items = [File]() mutating func add(item: File) { items.append(item) } }

现在是聪明的部分:当创建缓存时,我们显然可以直接创建一个特定的缓存,如下所示:

func loadDefaultCache() -> LocalFileCache { LocalFileCache(items: []) }

但很多时候我们想隐藏我们正在做的具体事情,如下所示:

func loadDefaultCacheOld() -> some Cache { LocalFileCache(items: []) }

使用 some Cache 使我们有灵活性改变我们发送回来的特定缓存,但 SE-0346 让我们做的是在具体类型绝对特定和不透明返回类型相当模糊之间提供一个中间地带。所以,我们可以专门化协议,如下所示:

func loadDefaultCacheNew() -> some Cache<File> { LocalFileCache(items: []) }

因此,我们仍然保留了将来转移到不同的 Cache-conforming 类型的能力,但我们已经明确表示无论选择什么都将在内部存储文件。

这种更智能的语法也扩展到其他地方,包括像扩展这样的东西:

extension Cache<File> { func clean() { print("Deleting all cached files…") } }

和泛型约束:

func merge<C: Cache<File>>(_ lhs: C, _ rhs: C) -> C { print("Copying all files into a new location…") // now send back a new cache with items from both other caches return C(items: lhs.items + rhs.items) }

但最有帮助的是,SE-0358 将这些主要关联类型带到了 Swift 的标准库中,所以 SequenceCollection 等都将受益 - 我们可以写 Sequence<String> 来编写与正在使用的确切序列类型无关的代码。

受约束的存在类型Existential Type

SE-0353提供了组合 SE-0309(所有协议protocol都可以作为存在类型Existential Type) 和 SE-0346(简化同类主关联类型) 的能力,可以写出any Sequence<String>

分布式的actor隔离

SE-0336SE-0344 引入了 actor 以分布式形式工作的能力 - 使用远程过程调用 (RPC) 在网络上读写属性或调用方法。

这个问题正如你想象的那样复杂,但有三点可以使它变得更容易:

  1. Swift 的位置透明性方法实际上迫使我们假设 actor 是远程的,事实上,在编译时无法确定 actor 是本地还是远程 - 无论如何,我们只需使用相同的 await 调用,如果 actor 恰好是本地的,那么调用将作为常规本地 actor 函数处理。
  2. 与其强迫我们构建自己的 actor 传输系统,Apple 为我们提供了一个现成的实现。虽然 Apple 表示他们“最终只期望有少量成熟的实现登上舞台”,但值得庆幸的是,Swift 中的所有分布式 actor 功能都与您使用的 actor 传输无关。
  3. 从 actor 转移到分布式 actor,我们主要只需要按需编写 distributed actor 然后 distributed func

因此,我们可以编写这样的代码来模拟有人追踪交易卡系统:

// use Apple's ClusterSystem transport typealias DefaultDistributedActorSystem = ClusterSystem distributed actor CardCollector { var deck: Set<String> init(deck: Set<String>) { self.deck = deck } distributed func send(card selected: String, to person: CardCollector) async -> Bool { guard deck.contains(selected) else { return false } do { try await person.transfer(card: selected) deck.remove(selected) return true } catch { return false } } distributed func transfer(card: String) { deck.insert(card) } }

由于分布式 actor 调用的抛出特性,我们可以确保在调用 person.transfer(card:) 没有抛出异常时,从一个收藏者中删除卡片是安全的。

Swift 的目标是让你可以很容易地将你对 actor 的了解转移到分布式 actor,但有一些重要的区别可能会让你感到困惑。

首先,所有分布式函数必须使用 tryawait 调用,即使函数没有标记为 throwing,因为由于网络调用出错,可能会发生故障。

其次,所有分布式方法的参数和返回值必须符合你选择的序列化过程,例如 Codable。这在编译时得到检查,因此 Swift 可以保证它能够从远程 actor 发送和接收数据。

第三,你应该考虑调整你的 actor API 以最小化数据请求。例如,如果你想要读取分布式 actor 的 usernamefirstNamelastName 属性,你应该更倾向于使用单个方法调用请求所有三个属性,而不是将它们作为单独的属性请求,以避免可能在网络上来回多次。

result builder 支持 buildPartialBlock

SE-0348 极大地简化了实现复杂`result builder`所需的重载,这也是 Swift 的高级正则表达式支持成为可能的部分原因。 SwiftUI 团队如果适配了这个特性,理论上也就消除了 SwiftUI 只最多 10 个 view 的限制。

为了给你一个实际的例子,这里是一个简化版的 SwiftUI 的 ViewBuilder

import SwiftUI @resultBuilder struct SimpleViewBuilderOld { static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View { TupleView((c0, c1)) } static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View { TupleView((c0, c1, c2)) } }

创建了两个 buildBlock() 的版本:一个接受两个视图,一个接受三个视图。实际上,SwiftUI 接受各种各样的替代方案,但关键是只能有 10 个。

然后,我们可以使用函数或计算属性来使用该结果构建器,如下所示:

@SimpleViewBuilderOld func createTextOld() -> some View { Text("1") Text("2") Text("3") }

这将使用 buildBlock<C0, C1, C2>() 变体接受所有三个 Text 视图,并返回包含它们所有的单个 TupleView。然而,在这个简化的示例中,没有办法添加第四个 Text 视图,因为我没有提供更多的重载,就像 SwiftUI 不支持 11 个或更多一样。

这就是新的 buildPartialBlock() 的作用,因为它的工作方式类似于序列的 reduce() 方法:它有一个初始值,然后通过将已经拥有的内容添加到接下来的内容来更新该值。

所以,我们可以创建一个新的结果构建器,它知道如何接受一个视图,以及如何将该视图与另一个视图组合:

@resultBuilder struct SimpleViewBuilderNew { static func buildPartialBlock<Content>(first content: Content) -> Content where Content: View { content } static func buildPartialBlock<C0, C1>(accumulated: C0, next: C1) -> TupleView<(C0, C1)> where C0: View, C1: View { TupleView((accumulated, next)) } }

尽管我们只有接受一个或两个视图的变体,但因为它们累积,我们实际上可以使用任意多个:

@SimpleViewBuilderNew func createTextNew() -> some View { Text("1") Text("2") Text("3") }

结果并不完全相同:在第一个示例中,我们将得到一个 TupleView<Text, Text, Text>,而现在我们将得到一个 `TupleView<(TupleView<(Text, Text)>, Text)> `- 一个 TupleView 嵌套在另一个 TupleView 内。

tips: buildPartialBlock() 是 Swift 的一部分,而不是任何特定平台运行时的一部分,所以如果你采用它,你会发现它可以部署到早期的操作系统版本。

隐式打开的存在类型

SE-0352 允许 Swift 在许多情况下使用协议调用泛型函数。

举个例子,这里有一个简单的泛型函数,能够处理任何类型的 Numeric 值:

func double<T: Numeric>(_ number: T) -> T { number * 2 }

如果我们直接调用它,例如 double(5),那么 Swift 编译器可以选择专门化该函数 - 出于性能原因,有效地创建一个直接接受 Int 的版本。

而SE-0352 的作用是允许在我们知道我们的数据符合协议的情况下调用该函数,如下所示:

let first = 1 let second = 2.0 let third: Float = 3 let numbers: [any Numeric] = [first, second, third] for number in numbers { print(double(number)) }

Swift 将这些称为存在类型:您正在使用的实际数据类型位于一个盒子内,当我们在该盒子上调用方法时,Swift 理解它应该隐式地在盒子内的数据上调用方法。SE-0352 也将这种功能扩展到函数调用:我们循环中的number 值是一个存在类型(一个包含 IntDoubleFloat 的盒子),但 Swift 能够将其传递给泛型 double() 函数,方法是传入盒子内的值。

这种功能有其局限性.例如,这种代码是无法工作的:

func areEqual<T: Numeric>(_ a: T, _ b: T) -> Bool { a == b } print(areEqual(numbers[0], numbers[1]))

Swift 无法在静态验证(即在编译时)中确定这两个值是否可以使用 ==进行比较,因此代码根本无法构建。

异步不可用属性

SE-0340 部分解决了 Swift 并发模型中可能存在的风险情况,允许我们将类型和函数标记为在异步上下文中不可用,因为这样使用它们可能会导致问题。除非您使用线程局部存储、锁、互斥量或信号量,否则您不太可能自己使用此属性,但您可能会调用使用此属性的代码,因此至少值得了解它的存在。

要将某个内容标记为在异步上下文中不可用,请使用 @available 和您通常选择的平台,然后在末尾添加 noasync。例如,我们可能有一个适用于任何平台的函数,但在异步调用时可能会导致问题,因此我们将这样标记它:

@available(*, noasync) func doRiskyWork() { }

然后,我们可以像往常一样从常规同步函数中调用它:

func synchronousCaller() { doRiskyWork() }

然而,如果我们尝试从异步函数执行相同的操作,Swift 将发出错误,因此这段代码将无法工作:

func asynchronousCaller() async { doRiskyWork() }

这种保护比当前的情况有所改善,但不应过分依赖它,因为它不会阻止我们嵌套调用我们的 noasync 函数,如下所示:

func sneakyCaller() async { synchronousCaller() }

这在异步上下文中运行,但调用同步函数,然后可以反过来调用 noasync 函数 doRiskyWork()

Swift 5.6

引入存在类型any关键词

SE-0335 引入了一个新的关键词 any 来标注存在类型(existential type)。这个特性有点难理解,也预示了 swift 后续版本的 break change。

协议允许我们指定符合的类型必须遵循的一组要求,例如它们必须要实现的方法。因此我们经常我们经常这么写:

protocol Vehicle { func travel(to destination: String) } struct Car: Vehicle { func travel(to destination: String) { print("I'm driving to \(destination)") } } let vehicle = Car() vehicle.travel(to: "London")

我们还可以在函数中将协议用作泛型的约束,这意味着我们可以编写满足指定协议的任何类型数据的代码。例如, 下面代码适用于任何满足Vehicle 协议的任何类型:

func travel<T: Vehicle>(to destinations: [String], using vehicle: T) { for destination in destinations { vehicle.travel(to: destination) } } travel(to: ["London", "Amarillo"], using: vehicle)

当上面代码编译时,Swift 可以感知到代码中调用了Cartravel( )方法,然后可以优化为直接调用travel() ——一种称之为静态调度(static dispatch)的流程。

这里还有一种相似的其他使用协议的方式:

let vehicle2: Vehicle = Car() vehicle2.travel(to: "Glasgow")

我们任然创建了Car的结构,但是把它存储为Vehicle。这不仅是简单的隐藏了底层信息,而是这个Vehicle成了一个称之为存在类型(existential type)全新的类型:能够容纳符合Vehicle协议的任何类型的任何值的新数据类型。

重要:存在类型(existential type)和通过some关键词声明的不透明类型(opaque type)不同。不透明类型(opaque type)表示一种未知的、特定的类型,该类型满足指定的约束。 尽管是未知的,但是编译器确保整个作用域内一致的使用相同的类型。因此不透明类型(opaque type)可以提供更强的类型保证和优化。

我们也可以在函数中使用存在类型(existential type),例如:

func travel2(to destinations: [String], using vehicle: Vehicle) { for destination in destinations { vehicle.travel(to: destination) } }

这种方式虽然和其他类型使用方式很像。但是函数中接受任何满足的`Vehicle`对象,Swift 就没办法做上述的优化了。它只能够通过一种动态调度(dynamic dispatch)的方式实现,因此也就没有上面静态调度高效。因此,Swift 目前版本中,协议的两种用法看起来非常相似,实际上较慢的存在版本的函数更容易编写。

为了解决这个问题,Swift 5.6 为存在类型(existential type)引入了any关键词,因此我们就可以代码中显示的指出了存在的影响。后续的版本如果不使用会告警。在 Swift 6 的可能就会报错,要求明确标注,按照下面的写法:

let vehicle3: any Vehicle = Car() vehicle3.travel(to: "Glasgow") func travel3(to destinations: [String], using vehicle: any Vehicle) { for destination in destinations { vehicle.travel(to: destination) } }

类型占位符

SE-0315引入了类型占位符的概念,能让我们在类型定义显示指定部分类型,让剩下的部分通过类型推断来填充。类型推断的地方使用_

类型占位符在编译器只能够正确推断部分类型时候比较有用。它可以简化类型的书写,而且还只支持可选,_?

var results3: [_: [Int]] = [ "Cynthia": [], "Jenny": [], "Trixie": [], ]

需要注意的是,目前类型占位符还不支持函数签名,但是你可以先写上_占位符,让编译器推断,然后让 Xcode 提供修复建议来完成代码。

容许key不是String/IntDictionary也能够使用KeyedContainer

SE-0320 引入了一个新的 CodingKeyRepresentable 协议,允许将具有非普通 String 或 Int 键的字典编码为键控容器,而不是非键控容器。这解决了以前在编码具有自定义枚举或结构键的字典时可能遇到的问题。

通过为自定义枚举或结构实现 CodingKeyRepresentable 协议,可以确保在编码和解码过程中正确处理这些键。这使得 JSON 的输出更易于理解和在 Swift 之外使用。

例如,下面的代码,Swift 能够正确执行:

import Foundation enum OldSettings: String, Codable { case name case twitter } let oldDict: [OldSettings: String] = [.name: "Paul", .twitter: "@twostraws"] let oldData = try JSONEncoder().encode(oldDict) print(String(decoding: oldData, as: UTF8.self))

但是打印是["twitter","@twostraws","name","Paul"] 明显不符合预期。

但是使用CodingKeyRepresentable协议后。

enum NewSettings: String, Codable, CodingKeyRepresentable { case name case twitter } let newDict: [NewSettings: String] = [.name: "Paul", .twitter: "@twostraws"] let newData = try! JSONEncoder().encode(newDict) print(String(decoding: newData, as: UTF8.self))

打印就符合预期了:{"twitter":"@twostraws","name":"Paul”}

https://stackoverflow.com/questions/68466494/what-is-a-swift-un-keyed-container

不可用#unavailable条件判断

SE-0290 引入了一个名为 #unavailable 的新指令,它与 #available 相反,当可用性检查失败时执行一些代码。这在你只想在特定操作系统不可用时运行代码的情况下非常有用。

与使用 #available 和空 true 块相比,现在可以更简洁地使用 #unavailable 指令:

if #unavailable(iOS 15) { // 使 iOS 14 和更早版本正常工作的代码 }

以前得通过空块 else方式:

if #available(iOS 15, *) { } else { // Code to make iOS 14 and earlier work correctly }

需要注意的是,与 #available 不同,#unavailable 不允许使用平台通配符 *,因为在这种情况下,它可能导致模糊性。

更多concurrency的改变

Swift 5.5 添加了许多关于并发的功能,而 5.6 继续完善这些功能,使它们更安全、更一致,同时为 Swift 6 中即将到来的更大、更突破性的变化做准备。

最大的变化是 SE-0337,旨在为我们的代码提供实现完全严格的并发检查的路线图。这是增量式的:你可以使用 @preconcurrency 导入整个模块,告诉 Swift 该模块是在没有考虑现代并发的情况下创建的;或者,你可以将 @preconcurrency 标记为单个类、结构、属性、方法等,以便更有选择性地使用。

另一个正在发生变化的领域是 actor 的使用,因为由于 SE-0327 的结果,Swift 5.6 现在会发出警告,如果你试图使用 @StateObject 实例化一个 @MainActor 属性,就像这样:

import SwiftUI @MainActor class Settings: ObservableObject { } struct OldContentView: View { @StateObject private var settings = Settings() var body: some View { Text("Hello, world!") } }

这个警告将在 Swift 6 中升级为错误,所以你应该准备好放弃这段代码,改用这个

struct NewContentView: View { @StateObject private var settings: Settings init() { _settings = StateObject(wrappedValue: Settings()) } var body: some View { Text("Hello, world!") } }

Swift 包管理的几个插件

Swift 5.6 包含了一系列针对 Swift Package Manager 的改进,这些改进结合起来添加了使用外部构建工具的插件支持的初步功能。


参考