What's new in Swift
Swift 进阶 45m

Swift 新特性

What's new in Swift

2025年6月9日

在 Apple 官方观看视频

一句话判断

如果你还在用 UnsafeBufferPointer 处理高性能数据,这场 Session 会给你一个终于可以扔掉它的理由。

这场 Session 讲了什么

Swift 6.2 把过去几年埋的雷一个个拆了。最显眼的是 Span 系列类型——它填补了 Swift 内存访问模型里那个尴尬的断层:Array 安全但有拷贝开销,UnsafeBufferPointer 高效但随时可能炸。Span 提供了一个零拷贝、零堆分配的只读视图,而且编译器帮你管生命周期。音频处理、图像算法、协议解析这些场景,你终于可以用纯 Swift 代码干 C 的活了。

InlineArray 解决的是另一个长期被忽视的问题:你存 4 个 Float 做个 RGBA 颜色,标准 Array 还是要走堆分配加引用计数。InlineArray<4, Float> 直接在栈上,没有间接寻址,没有 ARC 开销。在每帧调用数千次的渲染管线里,这个差距会被放大。

并发方面,Task(name:)Task.immediate() 看起来是小改动,但解决了真实的痛点。以前 Instruments 里一堆无名任务切换,根本分不清谁是谁。现在给任务起个名字,调试体验直接从”猜”变成”看”。immediate() 则允许任务跳过调度队列立即执行,对延迟敏感的用户交互场景有用——但别滥用,否则会破坏调度公平性。

值得深挖的点

Span 的生命周期设计:Swift 版的借用检查?

Span 最有意思的不是它的 API,而是它的约束。它是非拥有类型(non-owning),这意味着你拿到一个 Span,源集合必须在 Span 的生命周期内保持有效。这听起来像 Rust 的借用检查器,但 Swift 的实现方式完全不同——它依赖编译器的 region-based lifetime analysis 而不是显式的生命周期标注。

这个选择的 trade-off 很清晰:开发者心智负担低了很多,不需要像 Rust 那样在函数签名里写 <'a>。代价是 Swift 的保障不如 Rust 严格——Rust 在编译期就能抓住所有悬垂引用,Swift 在某些边缘情况下仍然需要运行时检查。但对大多数场景来说,这个折中是合理的。实际效果是:你调用 someArray.span 获得一个只读视图,传给下游函数做处理,整个过程没有拷贝,没有堆分配,编译器确保你不会在数组被释放后还访问这个视图。

RawSpanMutableSpan 把这个模型扩展到了原始字节和可写场景。UTF8Span 更进一步,专门为 UTF-8 字符串设计,绕过 String 的 copy-on-write 机制——在解析器和序列化器场景下,性能提升可以是数量级的。

InlineArray 的边界在哪

InlineArray 不是银弹。栈空间是有限的,通常只有几 MB,一个 InlineArray<1000, Float> 就占 4KB,嵌套几层递归就能栈溢出。所以它的甜蜜点在 2-16 个元素:3D 坐标、四元数、颜色分量、变换矩阵。

和 C 的固定数组相比,InlineArray 多了一层类型安全——你不能越界访问,编译器会检查。和 Rust 的 [T; N] 相比,语法上更简洁(InlineArray<4, Float> vs [f32; 4]),但泛型参数形式在嵌套时可读性会下降。一个值得注意的细节:InlineArray 的元素类型必须满足 Copyable(或至少在当前版本如此),这意味着你不能直接用它存引用类型——这既是限制也是保护,避免了栈上对象的复杂引用计数问题。

代码片段

// 用 Span 替代 Array 切片做零拷贝遍历
// 场景:解析网络收到的二进制协议数据
func parsePacket(_ data: [UInt8]) {
    let span = data.span          // 零拷贝,返回底层存储的只读视图
    for byte in span {
        processByte(byte)
    }
    let rawBytes = span.bytes     // 原始字节访问,可与 C API 交互
}
// 坑:Span 是非拥有类型,data 被释放后 span 会变成悬垂指针
// 用 InlineArray 消灭小数组的堆分配
// 场景:3x3 变换矩阵,每帧调用数千次
func applyTransform(_ m: InlineArray<9, Float>, to p: (Float, Float)) -> (Float, Float) {
    let (x, y) = p
    return (m[0]*x + m[1]*y + m[2], m[3]*x + m[4]*y + m[5])
}
// 坑:栈空间有限,别拿它存大数组(>16 元素就该考虑标准 Array)
// 给并发任务起名字,让 Instruments 调试不再抓瞎
// 场景:用户资料页并行加载多个数据源
await withTaskGroup(of: Data.self) { group in
    group.addTask(name: "load-cache") { await cacheService.load() }
    group.addTask(name: "fetch-network") { await networkService.fetch() }
}
// 坑:name 参数是 String,动态生成大量短任务时注意字符串分配开销

最佳实践

建议先在性能热点路径上试 Span——音频处理、图像像素遍历、协议解析这些地方。不要全局替换,先在 Instruments 里确认瓶颈在哪,然后只改那一段。Span 的非拥有特性要求更小心地管理生命周期,不要把它存成属性然后发现源集合已经被释放了。

InlineArray 的迁移更简单:搜索代码中的 Array<Float>Array<Double>,找出数组大小是编译时已知的地方。RGBA 颜色、3D 坐标、小矩阵这些,直接换成 InlineArray。改动小,收益明显。

任务命名是零成本改进,今天就可以做。给每个 TaskaddTask 加上 name 参数,等下次并发 bug 出现时会感谢自己。Task.immediate() 先别急着用——除非有明确的延迟敏感场景,否则默认的调度策略通常够好。

还有什么值得关注

  • RangeSetDiscontiguousSlice 让你可以高效表示和操作集合中不连续的多个区间,替代过去手动管理 [Range] 数组的繁琐方式
  • TaskExecutor 给高级用户提供了完全掌控任务调度策略的能力,配合 Task.immediate() 可以实现精细的并发控制
  • Swift 包管理器对 InlineArraySpan 的支持是开箱即用的,不需要额外配置
Swift Span InlineArray 并发 标准库