5/6/2025, 8:42:15 PM
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, // }
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
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)
这么写。
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
中只导入 Maps
,toRadians()
会正常工作。DetailView.swift
中导入了 GeoKit
,GeoKit
的 toRadians()
也会“泄漏”到整个项目,导致 ContentView.swift
中的代码突然可能抛出错误(因为编译器不知道用哪个版本)。Swift 6.1 改进了这里,通过启用 MemberImportVisibility
编译标志(可在 Xcode 中配置),Swift 6.1 引入了更严格的导入规则:
SE-0443提案新增了对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 组名
注意:
SE-0441提案对Swift版本的描述方式进行了重要调整,严格区分编译器版本和语言模式。
当前许多用户使用Swift 6编译器,但运行在Swift 5语言模式下(为兼容性禁用新特性)。此前命令行和文档中笼统的"Swift版本"表述容易引发混淆。
新规要点:
(例如:编译器版本=Swift 6,语言模式=Swift 5)
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开始算)。
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) }
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")) }
若需配置多个任务局部值,有两种选择:
withValue()
调用。@Test(.firstScope, .secondScope)
组合使用。Swift Testing 会按顺序执行作用域,后续作用域可覆盖先前设置的值。测试作用域是对现有功能的补充,仍可使用 init()
和 deinit()
进行设置/清理。不同之处在于,它们允许按需为单个测试或整个测试套件选择配置。
此外,还有更多变化可能影响您的开发工作:
整篇翻译自:https://www.hackingwithswift.com/articles/276/whats-new-in-swift-6-1