hiccLoghicc log by wccHipo日志

Swift 6.1 新特性

toc

Intro

尾部逗号

SE-0439调整了Swift的语法,在数组,字典,字典,元组,函数调用,泛型参数,字符串插值任何( )[ ]< >逗号分隔列表中支持了尾部逗号。

例如:

func add<T: Numeric,>(_ a: T, _ b: T,) -> T { a + b } let result = add(1, 5,) print(result,)

在正常使用中,会有类似下面的代码:

import Foundation let message = "Reject common sense to make the impossible possible." let range = message.range( of: "impossible", options: .caseInsensitive )

range(of:options)的调用函数,在Swift6.1之后,我们可以很方便的注释掉`options: .caseInsensitive` 这一整行而不会有语法错误。

注意:只有在括号等定界符中的列表才能用尾部逗号。下面的逗号场景是编译不通过的。

// enum IceCream { // case vanilla, chocolate, strawberry, // }

元类型 Key path

SE-0438拓展了Keypath的能力,支持了类型中的静态属性。

struct WarpDrive { static let maximumSpeed = 9.975 var currentSpeed = 8.0 }

currentSpeed可以使用let currentSpeed = \WarpDrive.currentSpeed 来获取。

但是静态属性maximumSpeed需要使用WarpDrive.Type

let maxSpeed = \WarpDrive.Type.maximumSpeed

当然可以在类型声明上使用:

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

自动推导TaskGroup 子任务结果类型

SE-0442 对TaskGroup做了一点改进,当使用withTaskGroup()withThrowingTaskGroup() 时候,可以跳过of参数,应为Swift可以自动推导出子任务返回类型。

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) }

在Swift 6.1之前,需要withTaskGroup(of: String.self)这么写。

容许nonisolated阻断全局actor推导

SE-0449 扩展了nonisolated 关键词,Swift 6.1之后可以应用到protocal,struct,class 和enum。让其阻断全局actor。

举个例子,我们一般会有个在特定actor的数据层,来保证数据的安全读写。

@MainActor class DataController { func load() { } func save() { } }

然后我们可以把它包裹在一个protocol,让遵循它的类型作为我们的controller,确保它们也在同样的actor。

@MainActor protocol DataStoring { var controller: DataController { get } }

现在任何遵循DataStoring的类型,都会自动推导为在同一个actor,就像DataController 我们可以不用await使用其方法。

struct App: DataStoring { let controller = DataController() init() { controller.load() } }

上面的global actor 继承推导链条比较简单,但是某些情况可能需要阻断这种情况,因此从 Swift 6.1 开始,我们可以将协议、结构体、类和枚举标记为 nonisolated,允许它们选择退出从其他地方继承的参与者隔离。这意味着你需要使用 await 等方式来访问原本可以直接使用的参与者隔离数据,例如:

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

成员导入可见性

SE-0444​ 改进了 Swift 的模块导入规则,使代码行为更加明确和一致。虽然这可能导致少量编译错误,但修复起来非常简单,最终会让代码更加可靠。

在 Swift 6.1 之前,如果一个模块(Module)在某个文件中被导入,它的部分 API 可能会“泄漏”到项目的其他文件中,导致意外的行为或冲突。

假设有两个框架:

  • Map 模块为 Double 添加了一个 toRadians() 方法(直接计算弧度)。
  • GeoKit 模块也添加了 toRadians(),但它的版本会检查 Double.nan 并抛出错误。
// Maps 模块的扩展 public extension Double { func toRadians() -> Double { self * .pi / 180 } } // GeoKit 模块的扩展 public extension Double { func toRadians() throws -> Double { guard !isNaN else { throw ConversionError.invalidNumber } return self * .pi / 180 } }
  • 如果在 ContentView.swift 中只导入 MapstoRadians() 会正常工作。
  • 但如果在 DetailView.swift 中导入了 GeoKitGeoKit 的 toRadians() 也会“泄漏”到整个项目,导致 ContentView.swift 中的代码突然可能抛出错误(因为编译器不知道用哪个版本)。

Swift 6.1 改进了这里,通过启用 MemberImportVisibility 编译标志(可在 Xcode 中配置),Swift 6.1 引入了更严格的导入规则:

  • 模块的 API 仅在显式导入的文件中可用,不再“泄漏”到其他文件。
  • 如果某个文件需要访问某个模块的功能,必须在该文件中显式导入它。

精细控制Swift编译器告警

SE-0443提案新增了对Swift编译器警告和错误的精细控制功能,比之前的"屏蔽警告"和"将警告视为错误"选项更灵活。

使用方法:

  • 在Xcode的"Swift编译器
    • 自定义标志"中添加-print-diagnostic-groups
    • 或命令行使用:swiftc -print-diagnostic-groups main.swift

效果示例: 当调用已弃用的函数时,警告信息会显示所属的诊断组(如[DeprecatedDeclaration]):

@available(macOS, deprecated: 15.0, renamed: "sequoiaFunction") func sonomaFunction() { } sonomaFunction() // 警告会标注[DeprecatedDeclaration]

控制方式:

  • 将特定警告升级为错误:添加-Werror DeprecatedDeclaration
  • 保持特定警告不受影响:添加-Wwarning 组名

注意:

  1. Xcode中每个标志需单独添加
  2. 标志的顺序会影响最终效果
  3. 完整文档可参考Swift诊断组说明,但部分内容可能尚未同步到Swift 6.1

明确区分「Swift语言模式」与编译器版本

SE-0441提案对Swift版本的描述方式进行了重要调整,严格区分编译器版本语言模式

当前许多用户使用Swift 6编译器,但运行在Swift 5语言模式下(为兼容性禁用新特性)。此前命令行和文档中笼统的"Swift版本"表述容易引发混淆。

新规要点:

  • 统一采用Swift语言模式指代代码使用的语言标准版本
  • 编译器版本与语言模式在文档和工具链中明确区分

(例如:编译器版本=Swift 6,语言模式=Swift 5)

Swift Test: confirmation 支持range

SE-0005 升级了confirmation()函数,不单支持单一固定值,还支持了range的完成次数。

例如,我们可能有一个简单的NewsLoader,来按需获取新闻,直到结束。

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 } }

Swift 6.1 测试confirmation()函数可以这样写:

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次,测试就算失败了。你可以使用半包的range:

@Test func atLeastFiveFeedsAreLoaded() async throws { let loader = NewsLoader() await confirmation(expectedCount: 5...) { confirm in for await _ in loader { confirm() } } }

但是不能用没有最小值的range,类似confirmation(expectedCount: ...10)因为这会有歧义,不确定是10次(从1开始算),还是11次(从0开始算)。

Swift 测试:从 #expect(throws:) 返回错误

ST-0006 提案废弃了 #expect(_:sourceLocation:performing:throws:) 和 #require(_:sourceLocation:performing:throws:) —— 它们原本使用尾随闭包执行待测代码,再用第二个尾随闭包验证抛出的错误是否符合预期。

从 Swift 6.1 开始,#expect(throws:) 和 #require(throws:) 已更新为直接返回所检查类型的错误,允许你将期望验证和错误评估分开处理。

例如,你可能有一段代码禁止在清晨或深夜玩电子游戏:

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

使用旧版废弃 API 时,检查具体错误类型的代码是这样的:

import Testing @Test func playGameAtNight() { #expect { try playGame(at: 22) } throws: { guard let error = $0 as? GameError else { return false } // perform additional error validation here return error == .disallowedTime } }

现在应迁移到新版写法,将期望验证和错误评估分离:

@Test func playGameAtNight() { // `error` will now be a GameError let error = #expect(throws: GameError.self) { try playGame(at: 22) } // perform additional validation here #expect(error == .disallowedTime) }

Swift 测试:测试作用域特性

ST-0007 引入了测试作用域特性,提供细粒度、并发安全的共享测试配置访问,从而能够为测试精确设置运行环境,避免竞态条件。

例如,我们从一个 Player 结构体开始:

struct Player { var name: String var friends = [Player]() @TaskLocal static var current = Player(name: "Anonymous") }

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

然后可以在生产代码中使用 Player.current,就像单例一样。实际上它并非单例,因为每个任务有独立的值,但这对代码透明。例如:

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

这是常规的 Swift 并发代码。测试作用域的关键在于:Swift Testing 高度依赖并发执行测试,每秒可运行数千个单元测试。通过测试作用域,我们可以为特定测试设置自定义的 Player.current 值,确保测试代码使用精确值,同时避免并发测试间的共享状态冲突。

创建测试作用域需遵循两个协议:核心的 TestTrait 和 Swift 6.1 引入的 TestScoping。后者要求实现 provideScope() 方法来配置测试环境。例如:

import Testing struct DefaultPlayerTrait: TestTrait, TestScoping { func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { let player = Player(name: "Natsuki Subaru") try await Player.$current.withValue(player) { try await function() } } }

关键部分是 Player.$current.withValue(player),它在调用测试函数时确保使用自定义的 Player.current 值。

为更好地集成,可添加 Trait 扩展:

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

然后即可在测试中使用:

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

若需配置多个任务局部值,有两种选择:

  1. 若需同时配置,可嵌套 withValue() 调用。
  2. 若需独立配置,可创建多个作用域并通过 @Test(.firstScope, .secondScope) 组合使用。Swift Testing 会按顺序执行作用域,后续作用域可覆盖先前设置的值。

测试作用域是对现有功能的补充,仍可使用 init() 和 deinit() 进行设置/清理。不同之处在于,它们允许按需为单个测试或整个测试套件选择配置。

此外,还有更多变化可能影响您的开发工作:

  • SE-0387 显著简化了跨平台编译流程——例如,现在可以直接在搭载 Apple Silicon 的 macOS 设备上构建面向 x86-64 架构的 Linux 二进制程序。
  • SE-0436 允许 Swift 替换来自 Objective-C 的实现代码,进一步提升与 Objective-C 的互操作性。
  • SE-0450 为 Swift 包引入了可选特性机制:包作者可以定义实验性 API 等可选功能模块,使用者在导入包时可自主选择启用所需特性。

整篇翻译自:https://www.hackingwithswift.com/articles/276/whats-new-in-swift-6-1