本文根据 Session 10164 What’s new in Swift 收拾。

1. 运用 if/else 和 switch 句子作为表达式

Swift 5.9 答应将 if/elseswitch 句子用作表达式,供给了一种新的收拾代码的办法。

假如咱们期望依据某些复杂的条件初始化 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 程序中。

WWDC23 - Swift 5.9 新功能速览

Swift 供给了固定的宏人物:

总的来说 Swift 宏能够分为两大类:

  1. Freestanding(独立宏):能够独立存在的宏,不依赖已有的代码。
  2. Attached(绑定宏):需求绑定到特定源码位置的宏,如特点、办法、类等。

其间, Freestanding 宏能够分为 Expression、Declaration。Attached 宏能够分为 Peer、Accessor、MemberAttribute、Member、Conformance。

WWDC23 - Swift 5.9 新功能速览

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 渠道上的单一共享完成:

WWDC23 - Swift 5.9 新功能速览

从 MacOS Sonoma 和 iOS 17 开端,DateCalendar 等基本类型、LocaleAttributedString 等格式化和国际化基本功能、 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%:

WWDC23 - Swift 5.9 新功能速览

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 代码中。

WWDC23 - Swift 5.9 新功能速览

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 protocolsprotocol 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。