现在位置: 首页 > Swift 教程 > 正文

Swift 版本新特性

Swift 从 2014 年发布到现在,语言面貌已经换过好几轮。

老教程里常见的 ++、C 风格 forprintln 之所以编译不过,就是版本差异造成的。

本文按版本梳理关键特性,重点讲 Swift 6 的严格并发和迁移路径,最后简单介绍 Swift 5.9 引入的宏。


版本时间线

先看一张总表,把「哪个版本带来了什么」对齐到时间轴上。

表中所有特性都能在 Swift Evolution 提案页查到,提案页会标注 Implemented in Swift X

版本时间关键特性
Swift 1.02014 年,随 Xcode 6首次发布,只能在 Apple 平台使用
Swift 2.02015 年,随 Xcode 7错误处理 try / catch;同年 12 月开源
Swift 3.02016 年,随 Xcode 8API 命名大改,首参数默认不带外部标签(SE-0046)
Swift 4.02017 年,随 Xcode 9Codable(SE-0166)、内存独占访问检查(SE-0176)
Swift 4.12018 年,随 Xcode 9.3自动合成 Equatable / Hashable(SE-0185)
Swift 4.22018 年,随 Xcode 10CaseIterable#error / #warning、随机数 API
Swift 5.02019 年,随 Xcode 10.2ABI 稳定;Result(SE-0235)、@unknown default(SE-0192)、原始字符串(SE-0200)
Swift 5.12019 年,随 Xcode 11不透明返回类型 some(SE-0244)、属性包装器(SE-0258)
Swift 5.22020 年,随 Xcode 11.4编译诊断改进、构建速度提升
Swift 5.32020 年,随 Xcode 12多尾随闭包(SE-0279)、@main(SE-0281)
Swift 5.42021 年,随 Xcode 12.5结果构造器 result builder(SE-0289),SwiftUI 声明式语法的基础
Swift 5.52021 年,随 Xcode 13async / await(SE-0296)、actor(SE-0306)、结构化并发(SE-0304)、Sendable(SE-0302)
Swift 5.62022 年,随 Xcode 13.3存在类型 any(SE-0335)
Swift 5.72022 年,随 Xcode 14if let 同名简写(SE-0345)、主关联类型(SE-0346)、并发检查的渐进迁移(SE-0337)
Swift 5.82023 年,随 Xcode 14.3语言特性的渐进启用机制进一步完善
Swift 5.92023 年,随 Xcode 15宏(SE-0382、SE-0389、SE-0397)、package 访问级别(SE-0386)、if / switch 表达式(SE-0380)、Observation(SE-0395)
Swift 5.102024 年,随 Xcode 15.3完整并发检查(complete concurrency checking)
Swift 6.02024 年,随 Xcode 16Swift 6 语言模式,数据竞争由警告升级为错误;区域隔离(SE-0414)、全局变量并发安全(SE-0412)、类型化 throws(SE-0413)
Swift 6.x6.0 之后在 6.0 基础上持续完善并发诊断与工具链

Swift 版本演进时间线:从 1.0 到 6.0 的关键特性,焦点为 5.5 的 async/await 与 6.0 的严格并发两次并发跃迁

注意:表中没有写具体版本号的小项,是因为官方提案页没有明确标注 Implemented in Swift X。写文档时不确定的版本号宁可不写,也不要编。


几个高频特性速览

下面挑出日常最常用、也最容易和旧教程冲突的几组特性,各配一段可运行的代码。

some 与 any

some 表示「某个具体的遵循类型,但我不告诉你它是谁」,编译期就确定了真实类型。

any 表示「任何遵循该协议的类型」,运行时才知道,代价是动态派发。

实例

import Foundation

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

实例

import Foundation

// 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 与任务组负责结构化并发。

实例

import Foundation

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 时,编译器会自动推导出遵循关系,不需要手写。

实例

import Foundation

// 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 模式下编译失败,因为全局可变状态没有隔离。

实例

import Foundation

// 一个非 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,编译器会自动保证它只在主线程被访问。

实例

import Foundation

// @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) 明确关掉某个变量的检查。

这个关键字等于对编译器说「我知道它不安全,责任在我」,所以必须配注释说明理由。

实例

import Foundation

// 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 Sendablenonisolated(unsafe)保留逃生舱,但必须写清理由

注意:Swift 6 语言模式不是强制的,Swift 5 模式仍受支持。但新项目建议直接开 Swift 6,越晚迁移成本越高。

更详细的数据竞争分析与 Sendable 规则见《Swift Sendable 与 Swift 6 严格并发》。


宏(Swift 5.9)简介

宏的作用是在编译期把一小段代码展开成另一段代码,运行时不产生额外开销。

它和 C 的宏有本质区别:Swift 宏是类型安全的,而且实现本身是一段普通 Swift 程序,在编译期由编译器调用。

Swift 5.9 引入的宏分两大类,见下表。

类型调用形式典型用途提案
独立宏#宏名(...)生成表达式或声明,如 #PredicateSE-0382、SE-0397
附加宏@宏名给已有声明补充成员或行为,如 @ObservableSE-0389

最常用的现成宏是 Observation 框架提供的 @Observable,它替你把属性观察代码全部生成出来。

实例

import Foundation
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

它对属性访问的追踪更细,只有真正被读取的属性才会触发视图更新。

宏会让编译变慢吗?

会有一点,因为编译器需要运行宏的实现代码。

换来的是运行时零开销,以及比手写样板代码更低的出错概率。