本文根据 Session 10164 What’s new in Swift 收拾。
1. 运用 if/else 和 switch 句子作为表达式
Swift 5.9 答应将 if/else
和 switch
句子用作表达式,供给了一种新的收拾代码的办法。
假如咱们期望依据某些复杂的条件初始化 let 变量,或许会写出这种难以阅览的复合三元表达式:
let bullet =
isRoot && (count == 0 || !willExpand) ? ""
: count == 0 ? "- "
: maxDepth <= 0 ? "▹ " : "▿ "
运用 If
表达式让咱们能够运用更易读的 if
句子链:
let bullet =
if isRoot && (count == 0 || !willExpand) { "" }
else if count == 0 { "- " }
else if maxDepth <= 0 { "▹ " }
else { "▿ " }
假如咱们有一个大局变量或存储特点:
let attributedName = AttributedString(markdown: displayName)
在咱们期望新增一个条件时,咱们有必要运用一个当即执行的闭包:
let attributedName = {
if let displayName, !displayName.isEmpty {
AttributedString{markdown: displayName)
} else {
"Untitled"
}()
运用 if
表达式能够留下更简练的代码:
let attributedName =
if let displayName, !displayName.isEmpty {
AttributedString(markdown: displayName)
} else {
"Untitled"
}
2. Result builder 的更精确的编译器确诊
Result builder 是驱动 SwiftUI 等功能的声明性语法。以前有过错的 Result builder 需求很长时刻才会提示失利,因为类型查看探究了许多或许的无效途径。
Swift 5.9 在其类型查看的功能、代码补全提示和过错信息提示上有进一步的优化,特别重视无效代码的优化。从 Swift 5.8 开端,无效代码的类型查看速度更快,而且无效代码的过错信息提示愈加精确。
以前某些无效代码或许会导致 Result builder 的不同部分出现误导性过错。例如在 Swift 5.7 中,以下代码的过错提示存在误导:
struct ContentView: View {
enum Destination { case one, two }
var body: some View {
List {
NavigationLink(value: .one) { // ⬅️⬅️⬅️⬅️⬅️⬅️
// The issue actually occurs here
Text("one")
}
NavigationLink(value: .two) {
Text("two")
}
}.navigationDestination(for: Destination.self) {
$0.view // ❌❌❌❌❌
// Value of type 'ContentView.Destination' has no member 'view'
}
}
}
过错原因是 NavigationLink
的结构办法要求 value
契合 Hashable
协议:
extension NavigationLink where Destination == Never {
// ...
public init<P>(value: P?, @ViewBuilder label: () -> Label) where P : Hashable
// ...
}
在 Swift 5.9 中,咱们会收到更精确的编译器确诊提示,然后定位问题:
struct ContentView: View {
enum Destination { case one, two }
var body: some View {
List {
NavigationLink(value: .one) { // ❌❌❌❌❌
// Cannot infer contextual base in reference to member 'one'
Text("one")
}
NavigationLink(value: .two) {
Text("two")
}
}.navigationDestination(for: Destination.self) {
$0.view
}
}
}
以上示例问题解决能够经过让
Destination
完成Hashable
协议:
enum Destination: Hashable { case one case two func hash(into hasher: inout Hasher) { switch self { case .one: hasher.combine(1) case .two: hasher.combine(2) } } }
3. 运用 Type Parameter Pack 支撑参数长度重载
泛型能够被编译器类型揣度。例如,数组类型运用泛型来供给数组 —— Arrat<Element>
,运用数组时只供给元素即可,无需指定显式参数,因为编译器能够依据元素值进行类型揣度。
下面是一个 evaluate()
API,它承受 Request
类型参数并生成强类型值。例如咱们经过 Request<Bool>
参数,能够回来 Bool
成果:
struct Request<Result> {
let result: Result
}
struct RequestEvaluator {
func evaluate<Result>(_ request: Request<Result>) -> Result {
return request.result
}
}
func evaluate(_ request: Request<Bool>) -> Bool {
return RequestEvaluator().evaluate(request)
}
假如一些 API 不只期望对详细类型进行笼统,还期望对传入的参数数量进行笼统。比方一个函数或许承受一个 request
并回来一个成果,或许承受两个 request
并回来两个成果:
let value = RequestEvaluator().evaluate(request)
let (x, y) = RequestEvaluator().evaluate(r1, r2)
let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
在 Swift 5.9 之前,完成此形式的唯一办法是为 API 支撑的每个特定参数长度添加重载:
func evaluate<Result>(_:) -> (Result)
func evaluate<R1, R2>(_:_:) -> (R1, R2)
func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3)
func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4)
func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5)
func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6)
但这种办法有局限性,传递的参数数量的上限是人为的,假如传递未界说数量的参数,则会导致编译过错。在上述示例中,因为没有能够处理超越 6 个参数的重载,但假如传递 7 个参数,导致编译过错:
let results = evaluator.evaluate(ri, r2, r2, r3, r4, r5,16, r7) // ❌❌❌❌❌
// Extra argument in call
在 Swift 5.9 中,泛型体系经过启用参数长度的泛型笼统,取得了对此 API 形式的支撑。 这是经过一种新的言语概念来完成的,该概念能够表明“Packed”在一起的多个独自的类型参数。 这个新概念称为 Type Parameter Pack。咱们需求做的是 <Result>
替换为 <each Result>
。
运用 Type Parameter Pack,将参数长度具有独自重载的 API 折叠为单个函数:
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
evaluate()
现在能够处理一切参数长度,不需求人为约束。该函数回来括号中的每个成果实例,该实例能够是单个值,也能够是包括每个值的元组:
struct Request<Result> {
let result: Result
}
struct RequestEvaluator {
func evaluate<each Result>(_ request: repeat Request<each Result>) -> (repeat each Result) {
return (repeat (each request).result)
}
}
let requestEvaluator = RequestEvaluator()
let result = requestEvaluator.evaluate(
Request(result: 1),
Request(result: true),
Request(result: "Hello"))
print(result)
// (1, true, "Hello")
调用现在能够处理任意数量参数的新函数,就像调用固定长度的重载函数相同。 Swift 依据咱们调用函数揣度每个参数的类型以及总数。更多有关 Type Parameter Pack 的内容,能够参阅 WWDC23 Session Generalize APIs with parameter packs。
4. 运用 Swift 宏进行 API 规划
Swift 5.9 供给了一个新的工具,运用新的宏体系进行富有表现力的 API 规划。经过宏来扩展言语自身的功能,消除样板文件并开释更多 Swift 的表达能力。以断语函数为例,它查看条件是否为 true
。 假如条件为 false
,断语将停止程序。产生这种情况时,开发者取得过错信息很少:
assert(max(a, b) == c)
咱们需求添加一些日志记载或进行调试中才干了解更多信息。 在 XCTest 中已经有了一些优化,能够获取两个值:
XCAssertEqual(max(a, b), c) // XCTAssertEqual failed: ("10") is not equal to ("17")
但咱们依然不知道这儿哪个值是过错的。 是 a、b 还是 max()
函数的成果? 这种方式不能进行一切的查看。Apple 之前未在 Swift 中改善这一点,但宏使其成为或许。在此示例中,assert()
语法讲被 #assert()
宏替换。 所以当断语失利时它能够供给更丰富的信息:
import PowerAssert
#assert(max(a, b)) // Type 'Int' cannot be a used as a boolean; test for '!= 0' instead
在 Swift 中,宏是 API,就像类型或函数相同,经过导入界说它们的 Module 来拜访它们,同时宏作为 Package 分发。 这儿的断语宏来自 PowerAssert 库,这是一个在 GitHub上供给的开源 Swift 包。咱们查看 PowerAssert 宏声明,它是用 macro
关键字引入的,但除此之外,它看起来很像一个函数:
public macro assert(_ condition: Bool)
有一个 Bool
参数用于查看条件,假如该宏产生一个值,则该成果类型将运用一般的 ->
箭头语法编写。
宏的运用将依据参数进行类型查看,假如咱们在运用宏时犯了过错,将在宏翻开之前当即收到有用的过错音讯,以可猜测的方式增强程序的代码。大多数宏被界说为“外部宏”,经过字符串指定宏完成的模块和类型:
public macro assert(_ condition: Bool) = #externalMacro(
module: “PowerAssertPlugin”,
type: “PowerAssertMacro"
)
外部宏在独立进程、安全沙盒的编译器插件的独自程序中界说,Swift 编译器从源码中提取宏的调用,转化为原始语法树传递给插件。宏的实验对原始语法树的翻开,并生成新的语法树。编译器插件将新的语法树序列化后刺进到源码,然后从头集成到 Swift 程序中。
Swift 供给了固定的宏人物:
总的来说 Swift 宏能够分为两大类:
- Freestanding(独立宏):能够独立存在的宏,不依赖已有的代码。
- Attached(绑定宏):需求绑定到特定源码位置的宏,如特点、办法、类等。
其间, Freestanding 宏能够分为 Expression、Declaration。Attached 宏能够分为 Peer、Accessor、MemberAttribute、Member、Conformance。
Swift 宏供给了一种新工具,作为更具表现力的 API 来消除 Swift 代码中的样板文件,然后协助开释 Swift 的表现力。宏对它们的输入进行类型查看,生成正常的 Swift 代码,并在程序中的界说点进行集成,因此它们的作用很简单推理。当咱们需求了解宏的作用时,Xcode 也支撑宏翻开后的源码进行断点调试。WWDC 23 Session Expand on Swift macros 和 Write Swift macros 供给了更多有关 Swift 宏的信息。
5. 运用 Swift 重写的 Foundation 结构
Swift 是一种可扩展的言语。这种可扩展性意味着咱们能够将 Swift 面向比 Objective-C 更广泛的当地,比方面向初级体系,而以前咱们或许需求运用 C 或 C++。 这意味着将 Swift 更清晰的代码和安全性保证带到更多当地。Apple 最近开源了用 Swift 重写的 Foundation 结构。这一行动将导致 Foundation 在 Apple 和非 Apple 渠道上的单一共享完成:
从 MacOS Sonoma 和 iOS 17 开端,Date
和 Calendar
等基本类型、Locale
和 AttributedString
等格式化和国际化基本功能、 JSON 编码和解码的都有了新 Swift 完成。而且功能方面的成功十分显着。
Calendar
核算重要日期的能力能够更好地运用 Swift 的值语义来防止中心分配,在某些基准测验中取得超越 20% 的改善。运用 FormatStyle
进行的 Date 格式化也取得了一些重大功能升级,与运用规范日期和时刻模板进行格式化的基准比较,有 150% 的巨大改善。
在 JSON 解码上。 JSONDecoder 和 JSONEncoder 全新的 Swift 完成,消除了与 Objective-C 调集类型之间高昂的转换代价。在 Swift 中解析 JSON 以初始化 Codable
类型的紧密集成也提高了功能。 在解析测验数据的基准测验中,新的完成速度快了 2 到 5 倍。这些改善不只来自于降低了从旧的 Objective-C 完成到 Swift 的桥接成本,还来自于根据 Swift 的新完成速度更快。
以一个基准测验为例,在 Ventura 中,因为桥接成本的原因,从 Objective-C 调用 enumerateDates
比从 Swift 调用稍快。 在 MacOS Sonoma 中,从 Swift 调用相同的功能要快 20%:
6. 运用 consuming 交出结构体的一切权
有时,在体系的较初等级上运转代码时,咱们需求更细粒度的操控才干完成必要的功能。Swift 5.9 引入了一些新功能,可协助咱们完成这种等级的操控。这些功能侧重于一切权的概念,即哪部分代码在应用程序中传递时“拥有”某个值。这儿有一个十分简单的 FileDescriptor
,为初级系的统调用供给了更好的 Swift 接口:
struct FileDescriptor {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func write(buffer: [UInt8]) throws {
let written = buffer.withUnsafeBufferPointer {
Darwin.write(fd, $0.baseAddress, $0.count)
}
// ...
}
func close() {
Darwin.close(fd)
}
}
但运用此 API 有一些简单出错的当地,例如咱们或许会在调用 close()
之后测验写入文件。或许忘记调用 close()
导致资源走漏。
一种解决方案是使其成为一个带有 deinit()
办法的类,当类型超出范围时主动调用 close()
。运用类也有一些缺点,例如进行额定的内存分配、引证语义导致导致竞争条件,或许无意中被存储。
实际上,运用结构体,结构体的行为也类似于引证类型。它保存一个引证真实值的整数,该值是一个翻开的文件。该类型的副本还或许导致在应用程序中共享可变状况,然后导致过错。咱们想要的是抑制仿制此结构体的能力。
Swift 类型,无论是结构体还是类,默许情况下都是可仿制的。大多数时候这是正确的。但有时这种隐式仿制并不是咱们想要的,特别是当仿制值时或许会导致正确性问题。 在 Swift 5.9 中,咱们能够运用这种新语法来做到这一点,该语法可应用于结构体和枚举,并抑制隐式仿制类型的能力。
一旦某个类型是不行仿制的,咱们就能够给它一个 deinit()
,就像给一个类相同,当该类型的值超出范围时,该办法将被运转:
struct FileDescriptor: ~Copyable {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func write(buffer: [UInt8]) throws {
let written = buffer.withUnsafeBufferPointer {
Darwin.write(fd, $0.baseAddress, $0.count)
}
// ...
}
func close() {
Darwin.close(fd)
}
deinit {
Darwin.close(fd)
}
}
close()
办法也能够标记为 consuming
,调用 consuming
办法或参数,会将值的一切权交给所调用的办法。 因为咱们的类型不行仿制,因此放弃一切权意味着咱们无法再运用该值:
consuming func close() {
Darwin.close(fd)
}
close()
被标记为 consuming
,而不是默许的 “borrowing”,那么 close()
一定是结构体的终究运用。这意味着,假如咱们先关闭文件,然后测验调用另一个办法,例如 write()
,咱们将在编译时收到过错音讯,而不是运转时失利。编译器还会指出消费运用产生在哪里:
let file = FileDescriptor(fd: descriptor)
file.close() // Compiler will indicate where the consuming use is
file.write(buffer: data) // ❌❌❌
// Compiler error: 'file' used after consuming
不行仿制类型是 Swift 体系级编程的一个强壮的新功能,但仍处于前期阶段。Swift 的更高版别将扩展泛型代码中的不行仿制类型。
7. Swift 与 C++ 的互操作性
许多应用程序具有用 C++ 完成的中心事务逻辑,与其交互并不那么简单。 一般,这意味着添加额定的桥接层,从 Swift 到 Objective-C,然后再到 C++。 Swift 5.9 引入了直接从 Swift 与 C++ 类型或函数交互的能力。 C++ 互操作性的工作方式就像 Objective-C 互操作性相同,将 C++ API 直接映射到 Swift 代码中。
C++ 拥有自己的类、办法、容器等概念。 Swift 编译器理解常见的 C++ 习惯用法,因此许多类型能够直接运用。例如,此 Person 类型界说了 C++ 值类型所需的 Copy 和 Move 结构函数、赋值运算符和析构函数:
// Person.h
struct Person {
Person(const Person &);
Person(Person &&);
Person &operator=(const Person &);
Person &operator=(Person &&);
~Person();
std::string name;
unsigned getAge() const;
};
std::vector<Person> everyone();
Swift 编译器将其视为值类型,并会在正确的时刻主动调用正确的特别成员函数。此外,vector 和 map 等 C++ 容器能够作为 Swift 调集进行拜访。咱们能够编写直接运用 C++ 函数和类型的简单 Swift 代码。 咱们能够在 std::vector<Person>
上运用 filter
等高阶函数,运用 C++ 的成员函数并直接拜访数据成员:
// Client.swift
func greetAdults() {
for person in everyone().filter { $0.getAge() >= 18 } {
print("Hello, (person.name)!")
}
}
C++ 运用 Swift 代码有与 Objective-C 运用 Swift 相同的机制。Swift 编译器将生成一个 generated header,其间包括 C++ 可见的 API。 然而与 Objective-C 不同的是,咱们不需求约束只能运用带有 @objc
特点注释的 Swift 类。C++ 能够直接运用大多数 Swift 类型和 API,包括特点、办法等,而无需任何桥接开销:
// Geometry.swift
struct LabeledPoint {
var x = 0.0, y = 0.0
var label: String = “origin”
mutating func moveBy(x deltaX: Double, y deltaY: Double) { … }
var magnitude: Double { … }
}
// C++ client
#include <Geometry-Swift.h>
void test() {
Point origin = Point()
Point unit = Point::init(1.0, 1.0, “unit”)
unit.moveBy(2, -2)
std::cout << unit.label << “ moved to “ << unit.magnitude() << std::endl;
}
在这儿咱们能够看到 C++ 如何运用 Swift 的 Point 结构。 包括生成的 Geometry-Swift.h
后,C++ 能够调用 Swift 初始化程序来创立 Point 实例、调用办法以及拜访存储和核算特点,一切这些都无需对 Swift 代码自身进行任何更改。
Swift 的 C++ 互操作性使得将 Swift 与现有 C++ 代码库集成变得比以往更简单。许多 C++ 习惯用法能够直接用 Swift 表达,Swift API 也能够直接从 C++ 拜访然后能够运用 C、C++ 和 Objective-C 的任意组合。有关更多信息,能够查看 WWDC 23 Session Mix Swift and C++。
8. Swift 并发中自界说 Actor 的同步机制
几年前 App 在 Swift 中引入了一种新的并发模型,根据 async/await、结构化并发和 Actor 等。 Swift 的并发模型是一个笼统模型,能够适应不同的环境和库。笼统模型有两个首要部分:Task 和 Actor。 Task 代表一个接连的工作单元,概念上能够在任何当地运转。 只需程序中有“await”,Task 就能够挂起,在 Task 能够继续时康复。Actor 是一种同步机制,供给对隔离状况的互斥拜访。 从外部进入 Actor 需求“await”,因为它或许会暂停使命。
Task 和 Actor 被集成到笼统言语模型中,但在该模型中,它们能够以不同的方式完成,然后适应不同的环境。Task 在大局并发池上执行。 大局并发池如何决定安排工作取决于环境。对于 Apple 的渠道,Dispatch 库为整个操作体系供给了优化的调度,而且针对每个渠道进行了不同的调整。但在约束性更强的环境中,多线程调度程序的开销或许不行承受。Swift 的并发模型是经过单线程协作行列完成的。因为笼统模型满足灵敏,相同的 Swift 代码能够映射到不同的运转时环境。
此外,与根据回调的库的互操作性,从一开端就内置于 Swift 的 async/await 支撑中。 withCheckedContinuation
操作答应暂停 Task,然后在响应回调时康复它。 这使得能够与自行管理使命的现有库集成:
withCheckedContinuation { continuation in
sendMessage(msg){ response in
continuation.resume(returning: response)
}
}
Swift 并发运转时,Actor 的规范完成是在 Actor 上执行的无锁使命行列。 假如该环境是单线程的,则不需求同步,但 Actor 模型也会维护程序的笼统并发模型。 咱们依然能够将相同的代码带到另一个多线程环境中。
在 Swift 5.9 中,自界说 Actor 答应特定 Actor 完成自己的同步机制。这使得 Actor 愈加灵敏而且能够适应现有环境。考虑一个管理数据库连接的 Actor,Swift 保证对该 Actor 的存储进行互斥拜访,因此不会对数据库进行任何并发拜访。
// Custom actor executors
actor MyConnection {
private var database: UnsafeMutablePointer<sqlite3>
init(filename: String) throws { … }
func pruneOldEntries() { … }
func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … }
}
await connection.pruneOldEntries()
但假如咱们需求更多地操控该怎么办?假如咱们想为数据库连接运用特定的调度行列(或许是因为该行列与未采用 Actor 的代码共享),该怎么办? 运用自界说 Actor Executor。咱们能够向 Actor 添加一个串行调度行列、一个 UnownedSerialExecutor
类型的核算特点,该特点生成该调度行列对应的 Executor:
actor MyConnection {
private var database: UnsafeMutablePointer<sqlite3>
// ⬇️⬇️⬇️
private let queue: DispatchSerialQueue
nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() }
// ⬆️⬆️⬆️
init(filename: String, queue: DispatchSerialQueue) throws { … }
func pruneOldEntries() { … }
func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … }
}
await connection.pruneOldEntries()
经过此更改,咱们的 Actor 实例的一切同步都将经过该行列进行。当咱们“awiat”从 Actor 外部调用 pruneOldEntries()
时,现在将在相应的行列上执行调度异步。这使咱们能够更好地操控各个 Actor 如何进行同步,甚至能够让 Actor 与没有运用 Actor 的其他代码同步,或许是因为它是用 Objective-C 或 C++ 编写的。
因为调度行列契合新的 SerialExecutor
协议,因此经过调度行列完成 Actor 的同步成为或许。咱们能够经过界说契合此协议的新类型来供给自己的同步机制,然后与 Actor 一起运用:
// Executor protocols
protocol Executor: AnyObject, Sendable {
func enqueue(_ job: consuming ExecutorJob)
}
protocol SerialExecutor: Executor {
func asUnownedSerialExecutor() -> UnownedSerialExecutor
func isSameExclusiveExecutionContext(other executor: Self) -> Bool
}
extension DispatchSerialQueue: SerialExecutor { … }
该新类型只要很少的中心操作:查看代码是否已经在 Executor 的上下文中执行。例如,是否在主线程上运转?以及提取 unownedExecutor
。Executor 最中心的操作是 enqueue()
,它获取执行者 job
的一切权。 job
是异步使命的一部分,需求在 Executor 上同步运转。 在调用 enqueue()
时,Executor 有责任在串行 Executor 上没有其他代码运转时运转该 job
。
Swift Concurrency 由 Task 和 Actor 组成的笼统模型涵盖了很大范围的并发编程使命。有关更多信息,能够参阅 WWDC23 Session Beyond the basics of structured concurrency。