Swift 版本新特性
Swift 从 2014 年发布到现在,语言面貌已经换过好几轮。
老教程里常见的 ++、C 风格 for、println 之所以编译不过,就是版本差异造成的。
本文按版本梳理关键特性,重点讲 Swift 6 的严格并发和迁移路径,最后简单介绍 Swift 5.9 引入的宏。
版本时间线
先看一张总表,把「哪个版本带来了什么」对齐到时间轴上。
表中所有特性都能在 Swift Evolution 提案页查到,提案页会标注 Implemented in Swift X。
| 版本 | 时间 | 关键特性 |
|---|---|---|
| Swift 1.0 | 2014 年,随 Xcode 6 | 首次发布,只能在 Apple 平台使用 |
| Swift 2.0 | 2015 年,随 Xcode 7 | 错误处理 try / catch;同年 12 月开源 |
| Swift 3.0 | 2016 年,随 Xcode 8 | API 命名大改,首参数默认不带外部标签(SE-0046) |
| Swift 4.0 | 2017 年,随 Xcode 9 | Codable(SE-0166)、内存独占访问检查(SE-0176) |
| Swift 4.1 | 2018 年,随 Xcode 9.3 | 自动合成 Equatable / Hashable(SE-0185) |
| Swift 4.2 | 2018 年,随 Xcode 10 | CaseIterable、#error / #warning、随机数 API |
| Swift 5.0 | 2019 年,随 Xcode 10.2 | ABI 稳定;Result(SE-0235)、@unknown default(SE-0192)、原始字符串(SE-0200) |
| Swift 5.1 | 2019 年,随 Xcode 11 | 不透明返回类型 some(SE-0244)、属性包装器(SE-0258) |
| Swift 5.2 | 2020 年,随 Xcode 11.4 | 编译诊断改进、构建速度提升 |
| Swift 5.3 | 2020 年,随 Xcode 12 | 多尾随闭包(SE-0279)、@main(SE-0281) |
| Swift 5.4 | 2021 年,随 Xcode 12.5 | 结果构造器 result builder(SE-0289),SwiftUI 声明式语法的基础 |
| Swift 5.5 | 2021 年,随 Xcode 13 | async / await(SE-0296)、actor(SE-0306)、结构化并发(SE-0304)、Sendable(SE-0302) |
| Swift 5.6 | 2022 年,随 Xcode 13.3 | 存在类型 any(SE-0335) |
| Swift 5.7 | 2022 年,随 Xcode 14 | if let 同名简写(SE-0345)、主关联类型(SE-0346)、并发检查的渐进迁移(SE-0337) |
| Swift 5.8 | 2023 年,随 Xcode 14.3 | 语言特性的渐进启用机制进一步完善 |
| Swift 5.9 | 2023 年,随 Xcode 15 | 宏(SE-0382、SE-0389、SE-0397)、package 访问级别(SE-0386)、if / switch 表达式(SE-0380)、Observation(SE-0395) |
| Swift 5.10 | 2024 年,随 Xcode 15.3 | 完整并发检查(complete concurrency checking) |
| Swift 6.0 | 2024 年,随 Xcode 16 | Swift 6 语言模式,数据竞争由警告升级为错误;区域隔离(SE-0414)、全局变量并发安全(SE-0412)、类型化 throws(SE-0413) |
| Swift 6.x | 6.0 之后 | 在 6.0 基础上持续完善并发诊断与工具链 |
注意:表中没有写具体版本号的小项,是因为官方提案页没有明确标注 Implemented in Swift X。写文档时不确定的版本号宁可不写,也不要编。
几个高频特性速览
下面挑出日常最常用、也最容易和旧教程冲突的几组特性,各配一段可运行的代码。
some 与 any
some 表示「某个具体的遵循类型,但我不告诉你它是谁」,编译期就确定了真实类型。
any 表示「任何遵循该协议的类型」,运行时才知道,代价是动态派发。
实例
protocol Greeter {
var name: String { get }
func greet() -> String
}
struct RunoobGreeter: Greeter {
let name: String
func greet() -> String { return "你好,\(name)" }
}
// some:返回某个具体的遵循类型,调用方不需要知道它是谁
func makeGreeter() -> some Greeter {
return RunoobGreeter(name: "Runoob")
}
print(makeGreeter().greet())
// any:把不同的遵循类型装进同一个数组,代价是动态派发
let greeters: [any Greeter] = [
RunoobGreeter(name: "www.runoob.com"),
RunoobGreeter(name: "RUNOOB")
]
for g in greeters { print(g.greet()) }
你好,Runoob 你好,www.runoob.com 你好,RUNOOB
更完整的取舍见《Swift some 与 any》。
if let 简写与 if 表达式
if let 同名简写省掉了重复的变量名,从 Swift 5.7 起可用(SE-0345)。
if / switch 可以直接作为表达式给变量赋值,从 Swift 5.9 起可用(SE-0380)。
实例
// if let 同名简写
var site: String? = "www.runoob.com"
if let site {
print("站点:\(site)")
}
// if 表达式:直接给 let 赋值
let score = 85
let level = if score >= 90 { "优秀" } else if score >= 60 { "及格" } else { "不及格" }
print(level)
// 主关联类型:返回类型写 some Collection<Int>,调用方拿到的是具体类型
func makeScores() -> some Collection<Int> {
return [60, 85, 90]
}
print(makeScores().count)
站点:www.runoob.com 及格 3
async / await 与 actor
Swift 5.5 引入的并发模型包含三块:async / await 负责挂起与恢复,actor 负责隔离可变状态,Task 与任务组负责结构化并发。
实例
actor Counter {
private var value = 0
func increment() { value += 1 }
func current() -> Int { value }
}
func fetchTitle(_ id: Int) async -> String {
return "文章 \(id)"
}
func run() async {
// async let:并发发起,在 await 处一起收结果
async let a = fetchTitle(1)
async let b = fetchTitle(2)
let titles = await [a, b]
print(titles)
// TaskGroup:任务数量动态时使用
let total = await withTaskGroup(of: Int.self) { group in
for id in 1...3 {
group.addTask { await fetchTitle(id).count }
}
var sum = 0
for await n in group { sum += n }
return sum
}
print("字符总数:\(total)")
// actor:内部状态被串行化访问,不会出现数据竞争
let counter = Counter()
await withTaskGroup(of: Void.self) { group in
for _ in 1...100 {
group.addTask { await counter.increment() }
}
}
print("计数:\(await counter.current())")
}
await run()
["文章 1", "文章 2"] 字符总数:12 计数:100
并发章节的完整内容见《Swift async 与 await》和《Swift actor 与数据隔离》。
Swift 6 严格并发与迁移建议
Swift 6 最大的变化不是新语法,而是把并发安全检查从「提醒」变成了「强制」。
数据竞争指的是两个线程同时访问同一块内存,其中至少一个是写操作,且没有同步措施。
这类 bug 往往在测试环境不复现,上线后才偶发崩溃,所以编译器宁愿在编译期拦住你。
Swift 6 语言模式做了什么
Swift 6 编译器同时支持 Swift 5 和 Swift 6 两种语言模式,通过 -swift-version 5 或 -swift-version 6 切换。
在 Swift 6 模式下,原本只是警告的并发问题会变成编译错误,构建直接失败。
| 检查项 | Swift 5 模式 | Swift 6 模式 |
|---|---|---|
| 跨隔离传递非 Sendable 值 | 警告 | 错误 |
| 全局可变状态 | 警告 | 错误(SE-0412) |
| actor 隔离属性被外部直接访问 | 警告 | 错误 |
| 闭包捕获非 Sendable 值 | 警告 | 错误 |
Sendable 与 actor 的配合
Sendable 是一个标记协议,表示「这个类型的值可以安全地跨隔离域传递」。
值类型且所有存储属性都是 Sendable 时,编译器会自动推导出遵循关系,不需要手写。
实例
// struct 的所有存储属性都是 Sendable 类型,编译器自动推导出 Sendable
struct RunoobConfig: Sendable {
let timeout: Int
let host: String
}
// 类型是 Sendable 的全局 let 常量,在 Swift 6 严格并发下安全
let defaultConfig = RunoobConfig(timeout: 30, host: "www.runoob.com")
actor Store {
private var items: [String] = []
func add(_ item: String) { items.append(item) }
func all() -> [String] { items }
}
func load(using config: RunoobConfig) async -> [String] {
let store = Store()
await store.add("\(config.host):\(config.timeout)")
return await store.all()
}
let items = await load(using: defaultConfig)
print(items)
把上面的代码用 Swift 6 语言模式运行,输出如下。
$ swift -swift-version 6 RunoobConfig.swift ["www.runoob.com:30"]
典型报错长什么样
下面这段代码在 Swift 5 模式下能跑,但在 Swift 6 模式下编译失败,因为全局可变状态没有隔离。
实例
// 一个非 Sendable 的引用类型
final class UnsafeBox {
var value = 0
}
// 全局可变状态:Swift 6 语言模式下会报错
var sharedBox = UnsafeBox()
func bump() {
sharedBox.value += 1
}
编译器给出的第一条诊断是:
error: var 'sharedBox' is not concurrency-safe because it is nonisolated global shared mutable state [#MutableGlobalVariable]
注意:上面这段代码故意不完整,就是为了展示真实的编译错误,不要直接拿去运行。这类全局可变状态正确的做法是收进 actor,或者改成 let 常量。
三种常见的修复手段
绝大多数报错可以归到三条路径:加隔离、加 Sendable、或者显式声明「由我负责」。
最推荐的是加隔离,把 UI 状态标成 @MainActor,编译器会自动保证它只在主线程被访问。
实例
// @MainActor:把类型的所有成员都绑定到主 actor,UI 状态就该这样写
@MainActor
final class RunoobViewModel {
var title = ""
func load() async {
title = "Swift 教程"
}
}
func run() async {
let vm = await RunoobViewModel()
await vm.load()
print(await vm.title)
}
await run()
Swift 教程
对于确实无法消除的旧代码,可以用 nonisolated(unsafe) 明确关掉某个变量的检查。
这个关键字等于对编译器说「我知道它不安全,责任在我」,所以必须配注释说明理由。
实例
// nonisolated(unsafe):明确声明「此处由我负责,不要按隔离规则检查」
nonisolated(unsafe) var legacyCounter = 0
func bump() {
legacyCounter += 1
}
bump()
bump()
print(legacyCounter)
2
从 Swift 5 迁移到 Swift 6 的步骤
不要指望一次性把所有警告清零,官方推荐的做法是分阶段推进。
关键是先让问题可见,再逐个模块收口。
| 步骤 | 做法 | 目的 |
|---|---|---|
| 1 | 先升到最新工具链,保持在 Swift 5 语言模式 | 排除语法层面的干扰 |
| 2 | 打开完整并发检查(-strict-concurrency=complete) | 让所有数据竞争问题以警告形式暴露出来 |
| 3 | 按 target 逐个启用 Swift 6 语言模式 | 一次只改一个模块,控制影响面 |
| 4 | 给跨隔离传递的数据类型加 Sendable | 让编译器确认数据可以安全跨线程 |
| 5 | 把 UI 与主线程状态标成 @MainActor | 明确隔离边界,减少手动锁 |
| 6 | 用 @preconcurrency import 隔离尚未适配的依赖 | 让第三方模块的警告不阻塞迁移 |
| 7 | 最后才考虑 @unchecked Sendable 或 nonisolated(unsafe) | 保留逃生舱,但必须写清理由 |
注意:Swift 6 语言模式不是强制的,Swift 5 模式仍受支持。但新项目建议直接开 Swift 6,越晚迁移成本越高。
更详细的数据竞争分析与 Sendable 规则见《Swift Sendable 与 Swift 6 严格并发》。
宏(Swift 5.9)简介
宏的作用是在编译期把一小段代码展开成另一段代码,运行时不产生额外开销。
它和 C 的宏有本质区别:Swift 宏是类型安全的,而且实现本身是一段普通 Swift 程序,在编译期由编译器调用。
Swift 5.9 引入的宏分两大类,见下表。
| 类型 | 调用形式 | 典型用途 | 提案 |
|---|---|---|---|
| 独立宏 | #宏名(...) | 生成表达式或声明,如 #Predicate | SE-0382、SE-0397 |
| 附加宏 | @宏名 | 给已有声明补充成员或行为,如 @Observable | SE-0389 |
最常用的现成宏是 Observation 框架提供的 @Observable,它替你把属性观察代码全部生成出来。
实例
import Observation
// @Observable 由 Observation 框架提供,为属性自动生成观察代码
@Observable
final class RunoobModel {
var title = "Swift 教程"
var viewCount = 0
}
let model = RunoobModel()
model.viewCount += 1
print("\(model.title) 阅读量:\(model.viewCount)")
Swift 教程 阅读量:1
如果你要自己写宏,需要建一个 SwiftPM 包:声明放对外模块,实现在另一个编译期模块里。
下面这段是声明部分的写法,属于纯语法片段,需要完整的宏包才能编译,不能单独用 swift 命令运行。
实例
@freestanding(expression)
public macro runoobURL(_ path: String) = #externalMacro(
module: "RunoobMacros", // 实现所在的编译期模块
type: "RunoobURLMacro" // 实现类型,负责生成代码
)
// 使用处:编译期展开成一段经过校验的 URL 字面量
let url = #runoobURL("https://www.runoob.com")
注意:宏的实现代码在编译期执行,可以读取源码结构,但不能访问运行时的数据。写自定义宏之前,先确认标准库或系统框架里有没有现成的,绝大多数场景都不需要自己实现。
常见问题
下面几个问题是版本升级过程中最常被问到的。
升级 Swift 版本需要重写代码吗?
大多数版本是向后兼容的,语法层面的破坏性改动主要集中在 Swift 2 到 3 那一轮。
Swift 6 的主要迁移成本在并发检查,而不是语法。
怎么确认某个特性从哪个版本开始可用?
查对应的 Swift Evolution 提案页,页面顶部会写 Implemented in Swift X。
也可以用 #available 判断运行时系统版本,但语言特性本身由编译器版本决定,不能靠运行时判断。
Swift 6 语言模式一定要打开吗?
不一定,Swift 5 语言模式仍受支持,两者可以在同一个工程里按 target 混合使用。
但严格并发检查能拦住的都是真问题,新项目建议直接开。
@Observable 和以前的 ObservableObject 有什么区别?
@Observable 是宏,编译器自动为每个属性生成观察代码,属性不需要再标 @Published。
它对属性访问的追踪更细,只有真正被读取的属性才会触发视图更新。
宏会让编译变慢吗?
会有一点,因为编译器需要运行宏的实现代码。
换来的是运行时零开销,以及比手写样板代码更低的出错概率。
