Highlight
Swift 5.7 让带关联类型的协议(protocol with associated type)可以与 existential
any类型直接交互,同时 opaque result type 支持主关联类型约束,where子句中的 same-type requirement 可以精确建模多个具体类型之间的等价关系。
核心内容
从类型擦除说起
你有一个农场,里面养着各种动物:鸡和牛。鸡下蛋,牛产奶。你想用一个统一的 Animal 协议来抽象它们,但每种动物产出的食物类型不同。
以前的做法是用关联类型(associated type)来建模这个差异。Chicken 的 CommodityType 是 Egg,Cow 的 CommodityType 是 Milk。
但当你把这些动物放进一个异构数组 any Animal 里时,问题就来了。调用 produce() 方法返回的关联类型,编译器该怎么处理?
(01:14)Swift 5.7 引入了 associated-type erasure。当关联类型出现在方法的返回位置(producing position)时,编译器会把它类型擦除到其上界(upper bound)。
protocol Food { }
protocol Animal {
associatedtype CommodityType: Food
func produce() -> CommodityType
}
struct Farm {
var animals: [any Animal]
func produceCommodities() -> [any Food] {
animals.map { animal in
animal.produce() // 返回类型被擦除为 any Food
}
}
}
关键点:
animal.produce()的返回类型是关联类型CommodityType- 在
any Animal上调用时,编译器将CommodityType擦除为any Food - 这是 Swift 5.7 的新能力,因为关联类型出现在 producing position
反过来,如果关联类型出现在参数位置(consuming position),类型擦除就不安全了。
protocol Animal {
associatedtype FeedType: AnimalFeed
func eat(_ food: FeedType) // consuming position
}
let animal: any Animal = Cow()
// animal.eat(someHay) // 编译错误!
(05:34)any AnimalFeed 不能安全转换为具体的 Hay 类型,因为具体类型在编译时未知。这时你需要把 existential 解包为 opaque some 类型。
Opaque Result Type 与主关联类型
(07:54)你的 hungryAnimals 属性用 lazy.filter 实现,避免了临时数组分配。但返回类型暴露了实现细节:LazyFilterSequence<Array<any Animal>>。
Swift 5.7 引入了 constrained opaque result type,让你既能隐藏具体类型,又能暴露足够的类型信息:
struct Farm {
var animals: [any Animal]
var hungryAnimals: some Collection<any Animal> {
animals.lazy.filter { $0.isHungry }
}
func feedAnimals() {
for animal in hungryAnimals {
// animal 的类型是 any Animal,可以调用 Animal 协议的方法
animal.eat(...)
}
}
}
关键点:
some Collection<any Animal>隐藏了LazyFilterSequence的具体类型- 但客户端仍然知道元素类型是
any Animal - 这依赖于
Collection协议声明了主关联类型(primary associated type)Element
你也可以在自己的协议上声明主关联类型:
protocol Container<Element> {
associatedtype Element
var count: Int { get }
}
Same-Type Requirement 建模类型关系
(14:54)现在场景更复杂了:喂动物之前要先种作物、收获、加工成饲料。Cow 吃 Hay,Hay 由 Alfalfa 加工而来。
你定义了 AnimalFeed 和 Crop 两个协议,各自有对方的关联类型。但这样写有个问题:
protocol AnimalFeed {
associatedtype CropType: Crop
static func grow() -> CropType
}
protocol Crop {
associatedtype FeedType: AnimalFeed
func harvest() -> FeedType
}
(19:40)编译器推导出的类型是 (some Animal).FeedType.CropType.FeedType,而 eat() 方法期望的是 (some Animal).FeedType。类型不匹配。
根本原因是协议定义太宽泛了,没有保证”种出来的作物加工后能得到原来的饲料类型”这个不变量。
Swift 5.7 的 same-type requirement 解决了这个问题:
protocol AnimalFeed {
associatedtype CropType: Crop
static func grow() -> CropType
}
protocol Crop {
associatedtype FeedType: AnimalFeed
func harvest() -> FeedType
}
extension AnimalFeed where Self.CropType.FeedType == Self { }
extension Crop where Self.FeedType.CropType == Self { }
关键点:
Self.CropType.FeedType == Self保证:从某种饲料种出来的作物,加工后得到的是同一种饲料- 这限制了哪些具体类型可以符合协议
feedAnimal()方法现在可以安全地链式调用grow()->harvest()->eat()
详细内容
类型擦除的方向性
Swift 5.7 的 associated-type erasure 有明确的方向规则:
// Producing position: 安全
func produce() -> CommodityType // 返回 any Food
// Consuming position: 不安全
func eat(_ food: FeedType) // 不能直接传入 any AnimalFeed
(04:41) producing position 的类型转换方向是”子类型转父类型”,总是安全的。consuming position 需要”父类型转子类型”,编译器无法保证。
用 some 解包 existential
当关联类型在 consuming position 时,你需要用 generic 函数或 opaque type 来解包:
// 用 generic 函数
func feedAnimal<AnimalType: Animal>(_ animal: AnimalType, with feed: AnimalType.FeedType) {
animal.eat(feed)
}
// 或者用 some
func feedAnimal(_ animal: some Animal, with feed: (some Animal).FeedType) {
// 这里需要更精确的类型匹配
}
Constrained Existential Types
Swift 5.7 之前,要表达”一个元素类型为 X 的 Collection”,你需要自己写类型擦除包装器。现在可以直接写:
// 以前
struct AnyCollection<Element>: Collection { /* 几十行样板代码 */ }
// Swift 5.7
func process(_ items: any Collection<MailmapEntry>) {
for item in items {
// item 是 MailmapEntry
}
}
(25:00)any Collection<MailmapEntry> 是内置的语言特性,不需要手写类型擦除包装器。
Same-Type Requirement 的完整示例
protocol Animal {
associatedtype FeedType: AnimalFeed
func eat(_ food: FeedType)
}
protocol AnimalFeed {
associatedtype CropType: Crop
static func grow() -> CropType
}
protocol Crop {
associatedtype FeedType: AnimalFeed
func harvest() -> FeedType
}
// 关键约束
extension AnimalFeed where Self.CropType.FeedType == Self { }
extension Crop where Self.FeedType.CropType == Self { }
// 现在 feedAnimal 可以安全编译
func feedAnimal(_ animal: some Animal) {
let crop = type(of: animal).FeedType.grow()
let feed = crop.harvest()
animal.eat(feed) // 类型匹配!
}
关键点:
type(of: animal).FeedType获取动物对应的饲料类型grow()返回CropType,harvest()返回FeedType- same-type requirement 保证
CropType.FeedType == Self,所以最终类型就是animal.FeedType
核心启发
1. 用 any 替代手写类型擦除包装器
以前为协议写 AnyXxx 包装器是 Swift 开发的常见痛点。Swift 5.7 的 any 语法让这成为历史。检查你项目中的类型擦除类,看看哪些可以直接换成 any SomeProtocol<SomeType>。
入口:any Collection<Element>、any Publisher<Output, Failure>
2. 为自定义集合协议声明主关联类型
如果你定义了类似 Container、DataSource 这样的协议,给它们加上主关联类型声明:
protocol DataSource<Item> {
associatedtype Item
func item(at index: Int) -> Item
}
这样调用方就可以写 some DataSource<User> 或 any DataSource<User>,既隐藏实现又保留类型信息。
3. 用 same-type requirement 建模双向关系
当你的数据模型中存在”A 关联 B,B 又关联回 A”的循环关系时,用 same-type requirement 保证一致性。典型场景:
- 数据库关系模型(Table -> Column -> Table)
- 图结构中的边和节点
- 编译器中的 AST 节点关系
入口:where Self.Other.AssociatedType == Self
关联 Session
- Embrace Swift generics — 本 session 的前置内容,讲解 Swift 泛型基础
- What’s new in Swift — Swift 5.7 新特性总览,包含
any/some语法和主关联类型 - Meet Swift Regex — Swift 5.7 字符串处理新工具,同样受益于语言改进
评论
GitHub Issues · utterances