3/21/2024, 8:27:07 AM
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
隔离的原则。通过引入隔离的默认值表达式,提案提供了一种更安全、更一致的方式来处理并发环境下的默认值,减少了潜在的并发错误,提高了代码的可读性和可维护性。
SE-0327 旨在加强 actor
的定义,明确 actor
实例的数据隔离何时开始和结束,以及在 actor
的 init
和 deinit
声明的主体内可以做什么。
actor
或全局 actor
隔离类型(GAIT)的非委托初始化器需要初始化该类型的所有存储属性。
虽然 actor
是引用类型,但它们的委托初始化器将遵循与值类型相同的基本规则,即:
self.init
的调用,那么它就是一个委托初始化器。不需要使用 convenience
关键字。self
之前,必须在所有路径上调用 self.init
。actor
和 class
类型之间这种差异的原因是 actor 不支持继承,因此它们可以去掉类初始化器委托的复杂性。GAIT 使用与普通类相同的语法形式来定义委托初始化器。
init
和 deinit
之间的唯一区别是,deinit 只能访问 Sendable 属性,而 init 可以在隔离衰减之前访问非 Sendable 属性。
主要内容包括:
这个提案的目标是通过明确 Actor 初始化器的行为和规则,确保 Actor 在并发环境下的正确性和安全性。它为 Actor 的初始化过程提供了清晰的指导方针,帮助开发者编写更加健壮和可靠的并发代码。
通过规范 Actor 初始化器的语义和约束,该提案旨在增强 Swift 并发模型的一致性和可预测性,提高开发者使用 Actor 的体验和效率。
if
和switch
表达式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-0382, SE-0389, SE-0397 为 Swift 带来了宏。宏是一种强大的工具,允许你创建在编译时转换代码的代码。
关键要点:
ExpressionMacro
和用于添加 getter 和 setter 的 AccessorMacro
,ConformanceMacro
用于使类型符合协议。使用宏的步骤:
使用宏:
但是也会:
例如 首先,我们需要创建执行宏展开的代码——将 #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 的示例。
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 抛出编译器错误 - 这是不允许的。
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
值无效。另一方面,因为消耗方法必须结束对象的生命周期,所以它们可以自由地改变其属性。
这种共享行为赋予了不可复制的结构体一种超能力,而这种超能力以前仅限于类和参与者:我们可以为它们提供析构器,当对不可复制实例的最后一个引用被销毁时,它将自动运行。
重要说明: 这与类上的析构器的行为略有不同,这可能是早期实现的小问题或有意的行为。
首先,这是一些使用类的析构器的代码:
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()
方法结束时。
在使用这个新功能时,你需要注意一些额外的复杂性:
~Copyable
,因为它会显著改变它们的使用方式。如果你在库中发布代码,这会破坏你的 ABI稳定。consume
运算符来终结变量绑定的生命周期SE-0366 将consume
扩展到可复制类型的局部变量和常量,这可能有利于那些希望避免在数据传递过程中幕后发生过多 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
行,它同时做了两件事:
newUser
复制到 userCopy
。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()
AsyncStream
和 AsyncThrowingStream
的便捷 makeStream
方法SE-0388 为 AsyncStream
和 AsyncThrowingStream
添加了一个新的 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
,每个子任务就会在其工作完成后自动销毁。
实际上,这个问题主要会出现在服务器代码中,服务器必须能够接受新连接,同时平稳地处理现有连接。
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 var
和 let
没有区别。要了解它实际有用的地方需要一个更长的代码示例,如下所示:
// 用户是活跃订阅者、非活跃订阅者,或者我们还不知道他们的状态。 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] {
的语法才行。
if let
快捷解可选包SE-0345 引入了一种新的简写语法,用于使用 if let
和 guard 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" } }
SE-0329 引入了一种新的、标准化的方式来引用 Swift 中的时间和持续时间。正如其名称所示,它分为三个主要组成部分:
对于许多人来说,最直接的应用将是新升级的 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 引入了一系列与正则表达式相关的改进,极大地改进了我们处理字符串的方式。
/.../
而不是通过 Regex
和字符串创建正则表达式的能力。 首先,我们有了一些新的字符串处理方法:
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") }
有一个关键的区别:
因此我们可以如下使用,字面量的方式。
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)) }
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 的标准库中,所以 Sequence
、Collection
等都将受益 - 我们可以写 Sequence<String>
来编写与正在使用的确切序列类型无关的代码。
Existential Type
SE-0353提供了组合 SE-0309(所有协议protocol
都可以作为存在类型Existential Type
) 和 SE-0346(简化同类主关联类型) 的能力,可以写出any Sequence<String>
。
actor
隔离SE-0336 和 SE-0344 引入了 actor 以分布式形式工作的能力 - 使用远程过程调用 (RPC) 在网络上读写属性或调用方法。
这个问题正如你想象的那样复杂,但有三点可以使它变得更容易:
await
调用,如果 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,但有一些重要的区别可能会让你感到困惑。
首先,所有分布式函数必须使用 try
和 await
调用,即使函数没有标记为 throwing,因为由于网络调用出错,可能会发生故障。
其次,所有分布式方法的参数和返回值必须符合你选择的序列化过程,例如 Codable
。这在编译时得到检查,因此 Swift 可以保证它能够从远程 actor 发送和接收数据。
第三,你应该考虑调整你的 actor API 以最小化数据请求。例如,如果你想要读取分布式 actor 的 username
、firstName
和 lastName
属性,你应该更倾向于使用单个方法调用请求所有三个属性,而不是将它们作为单独的属性请求,以避免可能在网络上来回多次。
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
值是一个存在类型(一个包含 Int
、Double
或 Float
的盒子),但 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()
。
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 可以感知到代码中调用了Car
的 travel( )
方法,然后可以优化为直接调用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 提供修复建议来完成代码。
String
/Int
的Dictionary
也能够使用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
不允许使用平台通配符 *,因为在这种情况下,它可能导致模糊性。
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 5.6 包含了一系列针对 Swift Package Manager 的改进,这些改进结合起来添加了使用外部构建工具的插件支持的初步功能。
参考