Highlight
Swift 5.7 让泛型代码更易写、更易读。Holly 用一个农场模拟器的例子,演示了从具体类型到函数重载、类继承、再到协议和泛型的完整抽象过程,并介绍了
some和any两种新语法在参数和集合中的不同用法。
核心内容
泛型是管理代码复杂度的基本工具。Holly 用一个农场模拟器贯穿全场,演示了代码从具体到抽象的完整演进过程。(00:17)
起点很简单:一个 Cow struct,一个 Hay struct,一个 Alfalfa struct,一个 Farm struct。Cow.eat 接受 Hay,Hay.grow 返回 Alfalfa,Alfalfa.harvest 返回 Hay。Farm.feed 先种苜蓿、再收割干草、最后喂牛。(01:48)
想加更多动物时,最直接的做法是函数重载:给 Farm 加 feed(horse:)、feed(chicken:)。但每个重载的实现几乎一样,只是类型不同。重载多了就成了样板代码。(02:46)
下一步尝试类继承:引入 Animal 基类,Cow、Horse、Chicken 继承它。但问题很快出现:每个动物吃不同的食物,Animal.eat 的参数类型写什么都别扭。写 Any 会失去类型安全,运行时可能传错食物;写泛型类参数会让所有引用 Animal 的地方都要带食物类型参数,非常累赘。(05:04)
协议是更好的抽象工具。Animal 协议有一个关联类型 Feed 和一个方法 eat(_ food: Feed)。每个具体动物类型符合 Animal 协议,编译器自动推断 Feed 关联类型。Cow 的 eat 接受 Hay,编译器就知道 Cow.Feed == Hay。(08:44)
Swift 5.7 引入了 some 关键字,让泛型参数声明更简洁。func feed(_ animal: some Animal) 等价于带 where 子句的显式泛型参数,但语法更干净。some 表示”一个具体的、符合该协议的类型”,底层类型在调用时确定,之后固定不变。(12:52)
如果需要存储不同类型的动物(比如一个数组里同时有牛和马),用 some 不行,因为 some 要求底层类型固定。这时用 any:[any Animal] 表示”任意符合 Animal 协议的类型”。any 是存在类型(existential type),底层类型可以在运行时变化。(21:04)
any Animal 和 some Animal 之间可以自动转换。在 feedAll 方法里,遍历 [any Animal] 时,每个元素会被自动解包(unbox)成 some Animal 传给 feed 方法,这样 feed 内部仍然可以使用关联类型。(23:00)
Holly 的建议是:默认写 some,只在需要存储任意类型时才改用 any。这和”默认用 let,需要修改时才用 var”是同样的思路。(26:12)
详细内容
从具体到抽象:识别重复模式
(01:48)初始代码只有具体类型:
struct Cow {
func eat(_ food: Hay) {}
}
struct Hay {
static func grow() -> Alfalfa {
Alfalfa()
}
}
struct Alfalfa {
func harvest() -> Hay {
Hay()
}
}
struct Farm {
func feed(_ cow: Cow) {
let crop = Hay.grow()
let produce = crop.harvest()
cow.eat(produce)
}
}
关键点:
- 每个类型职责清晰:Cow 吃,Hay 生长,Alfalfa 收割,Farm 喂养
- 但
feed只能喂 Cow,加 Horse 和 Chicken 需要重写
函数重载的尝试:
struct Farm {
func feed(_ cow: Cow) { ... }
func feed(_ horse: Horse) { ... }
func feed(_ chicken: Chicken) { ... }
}
关键点:
- 每个重载的实现几乎一样
- 加新动物类型就要加新重载
- 这是”临时多态”(ad-hoc polymorphism),不是通用解决方案
为什么类继承不适合这个场景
(05:04)尝试用类继承:
class Animal {
func eat(_ food: Any) {} // 问题:参数类型写什么?
}
class Cow: Animal {
override func eat(_ food: Any) {
guard let hay = food as? Hay else { ... }
// 喂干草
}
}
关键点:
- 强制使用引用语义,但动物实例不需要共享状态
- 子类必须 override,忘记的话运行时才发现
Any参数失去类型安全,运行时才检查- 如果用泛型类参数
Animal<Food>,所有引用都要带类型参数
用协议定义能力接口
(08:44)协议把”动物会吃食物”抽象成接口:
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
protocol AnimalFeed {
associatedtype CropType: Crop where CropType.Feed == Self
static func grow() -> CropType
}
protocol Crop {
associatedtype Feed: AnimalFeed where Feed.CropType == Self
func harvest() -> Feed
}
具体类型符合协议:
struct Cow: Animal {
func eat(_ food: Hay) {}
}
struct Hay: AnimalFeed {
static func grow() -> Alfalfa {
Alfalfa()
}
}
struct Alfalfa: Crop {
func harvest() -> Hay {
Hay()
}
}
关键点:
- 关联类型
Feed依赖于具体符合类型 Cow.eat接受Hay,编译器推断Cow.Feed == Hay- 协议不限制为 class,struct、enum、actor 都可以符合
- 编译器检查每个符合类型是否实现了所有要求
some 关键字:简化泛型参数
(12:52)传统泛型写法:
func feed<A: Animal>(_ animal: A) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
Swift 5.7 的简化写法:
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
关键点:
some Animal等价于显式泛型参数A: Animalsome表示”一个具体的、符合协议的类型”- 底层类型固定,编译器可以在函数体内使用关联类型
- 适用于参数位置和返回位置(如 SwiftUI 的
some View)
什么时候需要显式类型参数?当需要在签名中多次引用同一个类型时:
func buildHabitat<A: Animal>(for animal: A) -> A.Habitat {
// 参数类型和返回类型都依赖 A
}
any 关键字:存储任意类型
(21:04)some 要求底层类型固定,所以 [some Animal] 数组只能存同一种动物。要存混合类型,用 any:
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [any Animal]) {
for animal in animals {
feed(animal) // 自动解包
}
}
}
关键点:
any Animal是存在类型,可以存储任意符合 Animal 的具体类型- 类型擦除消除了具体类型之间的静态类型区别
- 但
any Animal不能直接访问关联类型,因为底层类型不固定 - Swift 5.7 支持自动解包:
any Animal参数可以传给some Animal参数 - 解包后,在
feed函数体内,底层类型固定,可以正常使用关联类型
some 和 any 的对比:
| 特性 | some | any |
|---|---|---|
| 底层类型 | 固定 | 可变 |
| 关联类型 | 可用 | 不可用(除非解包) |
| 异构集合 | 不支持 | 支持 |
| 可选值 | 必须指定底层类型 | 可以直接用 any Animal? |
| 性能 | 无类型擦除开销 | 有类型擦销开销 |
完整示例
(27:10)把所有概念组合起来的完整代码:
protocol AnimalFeed {
associatedtype CropType: Crop where CropType.Feed == Self
static func grow() -> CropType
}
protocol Crop {
associatedtype Feed: AnimalFeed where Feed.CropType == Self
func harvest() -> Feed
}
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [any Animal]) {
for animal in animals {
feed(animal)
}
}
}
struct Cow: Animal {
func eat(_ food: Hay) {}
}
struct Hay: AnimalFeed {
static func grow() -> Alfalfa {
Alfalfa()
}
}
struct Alfalfa: Crop {
func harvest() -> Hay {
Hay()
}
}
关键点:
AnimalFeed和Crop的关联类型互相约束,确保饲料和作物匹配Farm.feed用some Animal喂养单个动物Farm.feedAll用[any Animal]喂养混合动物群- 编译器保证不会喂错食物:你不能把
Hay传给吃Corn的Chicken
核心启发
-
做什么:审查项目中的函数重载,把重复实现改成泛型函数。
为什么值得做:Holly 的农场例子说明,3 个动物的feed重载实现几乎一样。改成func feed(_ animal: some Animal)后,加一个Sheep类型不需要改Farm的任何代码。
怎么开始:搜索项目中实现相似的重载函数,找出它们操作的共同能力(如都调用eat方法),提取成协议,用some参数写一个泛型版本。 -
做什么:用
some替换显式泛型参数,让代码更易读。
为什么值得做:func feed(_ animal: some Animal)比func feed<A: Animal>(_ animal: A)短一半,而且语义信息直接出现在参数声明里。Holly 强调这是 Swift 5.7 的常用模式。
怎么开始:遍历项目中的泛型函数,如果类型参数只在参数列表出现一次,改成some语法。如果类型参数在参数和返回类型都出现,保留显式参数。 -
做什么:用
any构建异构集合,但尽快解包回some。
为什么值得做:[any Animal]可以存混合类型,但内部操作需要关联类型时,类型擦除会挡住你。Holly 的feedAll演示了最佳实践:集合用any,遍历元素时传给接受some的函数,在函数体内恢复完整类型能力。
怎么开始:检查项目中用any(或隐式存在类型)的地方,找出需要访问关联类型的代码,把操作提取成接受some的函数,让编译器自动解包。
关联 Session
- Design protocol interfaces in Swift — learn how to design swift protocols that are easy to use and understand.
- Swift concurrency: Behind the scenes — dive deep into the swift concurrency runtime and learn how async/await and actors work under the hood.
- What’s new in Swift — discover the latest features and improvements in the swift programming language.
评论
GitHub Issues · utterances