WWDC Quick Look 💓 By SwiftGGTeam
Embrace Swift generics

Embrace Swift generics

观看原视频

Highlight

Swift 5.7 让泛型代码更易写、更易读。Holly 用一个农场模拟器的例子,演示了从具体类型到函数重载、类继承、再到协议和泛型的完整抽象过程,并介绍了 someany 两种新语法在参数和集合中的不同用法。

核心内容

泛型是管理代码复杂度的基本工具。Holly 用一个农场模拟器贯穿全场,演示了代码从具体到抽象的完整演进过程。(00:17

起点很简单:一个 Cow struct,一个 Hay struct,一个 Alfalfa struct,一个 Farm struct。Cow.eat 接受 HayHay.grow 返回 AlfalfaAlfalfa.harvest 返回 HayFarm.feed 先种苜蓿、再收割干草、最后喂牛。(01:48

想加更多动物时,最直接的做法是函数重载:给 Farmfeed(horse:)feed(chicken:)。但每个重载的实现几乎一样,只是类型不同。重载多了就成了样板代码。(02:46

下一步尝试类继承:引入 Animal 基类,CowHorseChicken 继承它。但问题很快出现:每个动物吃不同的食物,Animal.eat 的参数类型写什么都别扭。写 Any 会失去类型安全,运行时可能传错食物;写泛型类参数会让所有引用 Animal 的地方都要带食物类型参数,非常累赘。(05:04

协议是更好的抽象工具。Animal 协议有一个关联类型 Feed 和一个方法 eat(_ food: Feed)。每个具体动物类型符合 Animal 协议,编译器自动推断 Feed 关联类型。Coweat 接受 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 Animalsome 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: Animal
  • some 表示”一个具体的、符合协议的类型”
  • 底层类型固定,编译器可以在函数体内使用关联类型
  • 适用于参数位置和返回位置(如 SwiftUI 的 some View

什么时候需要显式类型参数?当需要在签名中多次引用同一个类型时:

func buildHabitat<A: Animal>(for animal: A) -> A.Habitat {
    // 参数类型和返回类型都依赖 A
}

any 关键字:存储任意类型

21:04some 要求底层类型固定,所以 [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 函数体内,底层类型固定,可以正常使用关联类型

someany 的对比:

特性someany
底层类型固定可变
关联类型可用不可用(除非解包)
异构集合不支持支持
可选值必须指定底层类型可以直接用 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()
    }
}

关键点:

  • AnimalFeedCrop 的关联类型互相约束,确保饲料和作物匹配
  • Farm.feedsome Animal 喂养单个动物
  • Farm.feedAll[any Animal] 喂养混合动物群
  • 编译器保证不会喂错食物:你不能把 Hay 传给吃 CornChicken

核心启发

  • 做什么:审查项目中的函数重载,把重复实现改成泛型函数。
    为什么值得做: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

评论

GitHub Issues · utterances