0%

Swift 6.1 有哪些新变化?

Swift 6.1 有哪些新变化?

列表中的尾随逗号、元类型键路径、诊断分组等

Paul Hudson

今年春季发布的 Swift 为语言带来了多项令人欢迎的增补和改进,可为许多项目带来细小却重要的优化。

诸如“列表允许尾随逗号”这样的特性早已出现在许多开发者的愿望清单中;而对 nonisolated 的扩展使用,也能绕过 Swift 并发中常见的一些痛点。不过总体来说,这些变化属于“润色级”更新,为 Swift 6.2 中的更大改动铺平道路。

下面,咱们来逐条看看变化内容……

小贴士
想亲自试代码示例?可将本文下载为 Xcode Playground


允许在逗号分隔列表中使用尾随逗号

SE-0439 调整了 Swift 语法:现在在数组、字典、元组、函数调用、泛型参数、字符串插值,乃至所有由圆括号 (), 方括号 [], 尖括号 <> 包围的列表中,都可以写尾随逗号:

1
2
3
4
5
6
func add<T: Numeric>(_ a: T, _ b: T,) -> T {
a + b
}

let result = add(1, 5,)
print(result,)

现实中我们多半不会这样写函数,但在多行参数调用里就非常实用:

1
2
3
4
let range = message.range(
of: "impossible",
options: .caseInsensitive,
)

从 6.1 起,你可以整行注释掉 options: 仍能编译通过——因为上一行的尾随逗号被允许。

注意
尾随逗号仅限“有定界符”的场景;因此下面的代码仍无法编译:

1
2
3
// enum IceCream {
// case vanilla, chocolate, strawberry,
// }

元类型(Metatype)键路径

SE-0438 扩展键路径以支持类型的静态属性,进一步增强其表达能力。

1
2
3
4
5
6
7
8
9
struct WarpDrive {
static let maximumSpeed = 9.975
var currentSpeed = 8.0
}

let current = \WarpDrive.currentSpeed // 实例属性
let max = \WarpDrive.Type.maximumSpeed // 静态属性

let specific: KeyPath<WarpDrive.Type, Double> = \.maximumSpeed

TaskGroup 的 ChildTaskResult 类型可被推断

SE-0442 在使用 withTaskGroup / withThrowingTaskGroup 时,可省略 of: 参数,Swift 会根据返回值自动推断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func printMessage() async {
let string = await withTaskGroup { group in
group.addTask { "Hello" }
group.addTask { "From" }
group.addTask { "A" }
group.addTask { "Task" }
group.addTask { "Group" }

var collected = [String]()
for await value in group { collected.append(value) }
return collected.joined(separator: " ")
}
print(string)
}

nonisolated 阻止全局 Actor 推断

SE-0449 现可在协议、结构体、类、枚举层面标记 nonisolated,以显式退出从其他地方继承而来的 Actor 隔离。

1
2
3
4
5
6
7
@MainActor
protocol DataStoring { var controller: DataController { get } }

nonisolated struct App2: DataStoring {
let controller = DataController()
init() async { await controller.load() } // 需要 await
}

成员导入可见性

SE-0444 清理了 import 规则,配合新构建标志 **MemberImportVisibility**,要求在每个 Swift 文件中显式写出所需模块的 import
这将修复“漏出”API 造成的名称冲突,但可能导致少量编译错误;解决方式是手动补齐 import。


更精确地控制编译警告

SE-0443 引入“诊断分组”概念,可针对特定 warning/​error 精细化配置 -Werror-Wwarning 等标志。

  1. Other Swift Flags-print-diagnostic-groups
  2. 观察编译输出末尾的 [GroupName]
  3. 追加 -Werror GroupName-Wwarning GroupName 进行升级 / 降级。

顺序很重要:先设全局,再覆写分组,或反之,结果不同。


统一“语言模式”术语

SE-0441 明确区分“Swift 编译器版本”与“Swift 语言模式”。
许多人使用 Swift 6 编译器 却仍处于 Swift 5 语言模式;如今官方文件及命令行选项将使用 language mode 描述后者。


Swift Testing:基于范围的确认(Range‑based confirmations)

ST-0005confirmation() 函数进行了升级,允许传入 完成次数区间,而不再局限于单一固定值。


示例:按需加载新闻源

假设我们有一个简单的 NewsLoader,会按需抓取新闻,直到不再有新的源可取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct NewsLoader: AsyncSequence, AsyncIteratorProtocol {
var current = 1

mutating func next() async -> Data? {
defer { current += 1 }

do {
let url = URL(string: "https://hws.dev/news-\(current).json")!
let (data, _) = try await URLSession.shared.data(from: url)
return data.isEmpty ? nil : data
} catch {
return nil
}
}

func makeAsyncIterator() -> NewsLoader {
self
}
}

在编写测试时,我们也许事先知道期望恰好加载 5 条新闻;但事实上,只要返回 至少 5 条 也能接受。自 Swift 6.1 起,confirmation() 现在可接受区间参数:

1
2
3
4
5
6
7
8
9
10
11
import Testing

@Test func fiveToTenFeedsAreLoaded() async throws {
let loader = NewsLoader()

await confirmation(expectedCount: 5...10) { confirm in
for await _ in loader {
confirm()
}
}
}
  • confirm() 被调用 少于 5 次多于 10 次,测试即失败。
  • 你也可以用“半开区间”,例如确保至少调用 5 次:
1
2
3
4
5
6
7
8
9
@Test func atLeastFiveFeedsAreLoaded() async throws {
let loader = NewsLoader()

await confirmation(expectedCount: 5...) { confirm in
for await _ in loader {
confirm()
}
}
}

⚠️ 不允许省略下界(如 confirmation(expectedCount: ...10)),以避免歧义——无法确定是“最多 10 次(从 1 计数)”,还是“最多 11 次(从 0 计数)”。


Swift Testing:从 #expect(throws:) 返回错误对象

ST-0006 弃用了旧版
#expect(_:sourceLocation:performing:throws:)#require(_:sourceLocation:performing:throws:)
它们曾使用两个尾随闭包:第一个执行待评估代码,第二个检验捕获的错误。

自 Swift 6.1 起,新的 #expect(throws:)#require(throws:) 直接返回被检查的错误类型,方便将“断言”与“错误验证”分离。


示例:限制游戏时间

1
2
3
4
5
6
7
8
9
10
11
enum GameError: Error {
case disallowedTime
}

func playGame(at time: Int) throws(GameError) {
if time < 9 || time > 20 {
throw GameError.disallowedTime
} else {
print("Enjoy!")
}
}

旧 API(已弃用)

1
2
3
4
5
6
7
8
9
10
11
import Testing

@Test func playGameAtNight() {
#expect {
try playGame(at: 22)
} throws: {
guard let error = $0 as? GameError else { return false }
// 在此进行附加验证
return error == .disallowedTime
}
}

新 API

1
2
3
4
5
6
7
8
9
@Test func playGameAtNight() {
// `error` 现为 GameError
let error = #expect(throws: GameError.self) {
try playGame(at: 22)
}

// 单独执行进一步校验
#expect(error == .disallowedTime)
}

这些改动让 Swift 6.1 的测试 API 更加灵活、直观——无论是对可预期的调用次数设上限/下限,还是将错误校验解耦,均能编写出更清晰的测试代码。

Swift Testing:测试作用域特性(Test Scoping Traits)

ST-0007 引入了“测试作用域特性”,它为共享的测试配置提供了细粒度、并发安全的访问方式,使我们能够在不产生竞态条件的前提下,为特定测试构建精确的执行环境。


基本示例

1
2
3
4
5
6
struct Player {
var name: String
var friends = [Player]()

@TaskLocal static var current = Player(name: "Anonymous")
}

请注意 current 属性上的 @TaskLocal 标记。
Swift 并发不允许直接创建共享的可变状态,而 @TaskLocal 通过在 每个任务内部 放置一个并发安全的共享实例来解决这一问题:同一任务中的代码可以安全地读取这个值,而其他任务则拥有各自独立的 Player 实例。

在实际代码中,你可以像使用单例一样访问 Player.current(虽然它并不是真正的全局单例——每个并发任务都有自己的值):

1
2
3
4
5
func createWelcomeScreen() -> String {
var message = "Welcome, \(Player.current.name)!\n"
message += "Friends online: \(Player.current.friends.count)"
return message
}

到目前为止,这只是常规的 Swift 并发用法。接下来就轮到 测试作用域 发挥作用了:Swift Testing 默认并发执行大量单元测试。通过作用域,我们可以在某些测试外围包裹一个自定义的 Player.current,从而保证该测试内部使用的值精确且隔离,不会与其他并发测试产生冲突。


创建测试作用域

要创建一个作用域,需要同时遵循两个协议:

  1. **TestTrait**(核心特性协议)
  2. **TestScoping**(Swift 6.1 新增,负责具体作用域逻辑)

TestScoping 唯一必须实现的方法是 provideScope()——它负责为测试配置环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Testing

struct DefaultPlayerTrait: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: () async throws -> Void
) async throws {
// 1. 创建具有特定值的 Player
let player = Player(name: "Natsuki Subaru")

// 2. 作为 Task-local 注入,并执行测试
try await Player.$current.withValue(player) {
try await function()
}
}
}

重点在最后一行:Player.$current.withValue(player) 会在整个测试执行期间,把 Player.current 设为我们自定义的值。


让特性更易用

为了让自定义特性与内置特性保持一致,可添加一个简短的扩展:

1
2
3
extension Trait where Self == DefaultPlayerTrait {
static var defaultPlayer: Self { Self() }
}

在测试中使用作用域

1
2
3
4
@Test(.defaultPlayer) func welcomeScreenShowsName() {
let result = createWelcomeScreen()
#expect(result.contains("Natsuki Subaru"))
}

只需在 @Test 标记中写 .defaultPlayer,框架就会自动启用该作用域;运行 createWelcomeScreen() 时,Player.current 将是我们在作用域里设置的自定义值,而非默认值。


多个 Task-local 值

如果需要配置多个 Task-local 值,有两种方式:

  1. **嵌套 withValue()**:若多个值必须同时生效,可将调用层层嵌套。
  2. 多个作用域:如果值之间可以独立组合,则可创建多个作用域,并在测试标签里写 @Test(.firstScope, .secondScope, .thirdScope)。Swift Testing 会按列出顺序依次应用作用域,后面的作用域可以覆盖前面的设置。

与现有特性协同

测试作用域用于 补充 而非替代现有的 Swift Testing 机制——例如,你仍可在 init()/deinit() 里执行自定义的测试准备与清理工作。
区别在于:作用域让我们可以对单个测试或整组测试按需开启环境配置,且在并发运行时天然避免竞态条件。


还有更多……

除了上文提到的内容,Swift 6.1 还有其他值得关注的更新:

  • SE-0387:显著简化跨平台构建,例如在 Apple Silicon 的 macOS 上编译 x86-64 Linux 可执行文件。
  • SE-0436:允许 Swift 替换 Objective-C 的实现代码。
  • SE-0450:Swift Package 可声明“可选特性”,使用者可按需启用或移除(如实验性 API)。

总体而言,Swift 6.1 虽非“革命性”版本,但通过对子任务类型推断、导入可见性、元类型键路径等细节的打磨,使语言更加一致、易用。
Swift 6.2 预计将在 WWDC 25 登场,届时并发模型等方面将迎来进一步改进,值得期待。

翻译自What’s new in Swift 6.1?