为什么 Swift 5.7 再次"颠覆"协议
在 Swift 5.7 之前,带关联类型的协议只能当约束 <T: Sequence>,不能当类型 Sequence。
这导致两个老大难:
- 声明变量/参数/返回值时,必须再包一层类型擦除(
AnySequence<Int>)。 - 泛型函数无法"一眼看出"序列里到底是什么元素。
Swift 5.7 一次性给出三把新钥匙:
| 特性 | 关键词 | 解决痛点 |
|---|---|---|
| 主要关联类型 | protocol Sequence<Element> |
把最常用的关联类型"提级"到协议名上 |
| 不透明参数 | some Sequence<Int> |
直接当参数类型,编译期确定,零运行时开销 |
| 存在性类型 | any Sequence<Int> |
真·变量类型,运行时盒子,语法终于可读 |
Primary Associated Type:把"泛型参数"搬到协议头上
- 标准库示例
swift
// Swift 5.7 标准库定义
protocol Sequence<Element> {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
func makeIterator() -> Iterator
}
Element 被写到协议名后面,成为主要关联类型。
于是我们可以像泛型结构体一样,直接写:
swift
func sum(_ numbers: some Sequence<Int>) -> Int { // ① 参数类型
numbers.reduce(0, +)
}
let total = sum([1, 2, 3] + [4, 5, 6]) // ② 数组拼接也适用
- 为自己协议添加"主关联类型"
swift
protocol Feed<Element> { // ① 把最常用的关联类型提出来
associatedtype Element
mutating func next() -> Element?
}
struct IteratorFeed: Feed {
var a = 0, b = 1
mutating func next() -> Int? {
let next = a
a = b
b = next + a
return next
}
}
// ② 立刻享受 some/any 语法
func makeIntFeed() -> some Feed<Int> {
IteratorFeed()
}
var feeds: [any Feed<Int>] = [] // ③ 数组里放不同实现,无需再包 AnyFeed
规则
- 只能把一个或多个
associatedtype声明为"主要",不强制全部。 - 主要关联类型顺序不影响使用,但建议按"常用度"排序。
- 一旦声明,协议就获得"泛型形参"资格,可直接写
Feed<Int>。
some vs. any:一句话区分
| 维度 | some P |
any P |
|---|---|---|
| 语义 | 编译期确定的某个具体类型 | 运行时存在的任何具体类型 |
| 内存布局 | 无间接寻址,内联存储 | 存在性容器(Box),通过指针引用 |
| 主要用法 | 返回值、泛型约束 | 变量、集合元素、函数参数 |
| 性能 | 零额外开销 | 轻微运行时开销(Box管理) |
| 使用限制 | 不能用于var/集合类型声明 |
无显式限制 |
示例对比
swift
protocol Feed<Element> { // ① 把最常用的关联类型提出来
associatedtype Element
mutating func next() -> Element?
}
struct IteratorFeed<Iterator: IteratorProtocol>: Feed {
var iterator: Iterator
init(_ iterator: Iterator) {
self.iterator = iterator
}
mutating func next() -> Iterator.Element? {
iterator.next()
}
}
// 1. 返回值:编译期就知道真实类型
func uniqueElements<S: Sequence>(_ seq: S) -> some Sequence<S.Element>
where S.Element: Hashable & Comparable {
Set(seq).sorted()
}
// 2. 数组:运行期才知道真实类型
var parsers: [any Feed<Int>] = [
IteratorFeed([1, 2, 3].makeIterator()),
IteratorFeed(stride(from: 0, to: 10, by: 2).makeIterator())
]
实战:一行代码写"泛型" SwiftUI View
swift
import Charts
struct ChartView<Data: RandomAccessCollection>: View where Data.Element == Double {
let data: Data
var body: some View {
// 老写法:调用方必须写冗长泛型参数
}
}
// ✅ 新写法:直接不透明返回,调用方无感知
func makeChart(_ data: some RandomAccessCollection<Double>) -> some View {
Chart {
ForEach(data.enumerated(), id: \.offset) { idx,value in
LineMark(x: .value("x", idx), y: .value("y", value))
}
}
}
迁移旧代码:把 AnySequence 换成 any Sequence
| 旧代码 | 新代码 |
|---|---|
AnySequence<Int> |
any Sequence<Int> |
AnyPublisher<Int, Never> |
any Publisher<Int, Never> |
AnyView |
any View(iOS 17+ 可用,仍需权衡性能) |
步骤
- 在协议名后加
<Element>。 - 把
eraseToAnyPublisher()/AnySequence(...)改成any Sequence<Int>。 - 若返回值无需运行时多态,直接用
some Sequence<Int>获得零开销。
性能与二进制大小小贴士
some P<T>完全静态派发,零额外内存。any P<T>引入存在性容器,16 字节 inline + 外挂堆(若值过大)。- 滥用
any会让二进制出现大量"协议见证表",release 模式下编译器会优化,但debug 增量编译可能变慢。 - 对 SwiftUI
body这种超高频调用,优先some View;只在需要运行时异构数组时才用any View。
总结
| 关键词 | 适用场景 | 记忆口诀 |
|---|---|---|
protocol P<Element> |
给自己协议"提级" | "协议也能带泛参" |
some P<T> |
返回值、泛型约束 | "编译期就确定" |
any P<T> |
变量、数组、字典 | "运行期多态盒子" |
一句话总结
Swift 5.7 让协议第一次真正拥有了"泛型形参"能力:
- 写库的人:给协议加
<Element>,调用方立刻享受some/any语法糖。 - 写业务的人:用
some Sequence<Int>替代冗长泛型约束,用any Sequence<Int>替代AnySequence,代码短一半、可读性翻倍。