Generalize APIs with parameter packs
System & Services 进阶 20m

用参数包泛化 API 设计

Generalize APIs with parameter packs

2023年6月5日

在 Apple 官方观看视频

一句话判断

Swift 5.9 的参数包(Parameter Packs)终结了泛型 API 中”为每个参数数量写一遍重载”的痛苦模式——现在你可以写一份泛型代码,同时保持每个参数的静态类型信息不被擦除。

这场 Session 讲了什么

Swift 编译器团队的 Sophia 深入讲解了 Swift 5.9 中引入的参数包特性。这是一个高级泛型编程特性,解决了长期以来 Swift 泛型系统的一个根本性限制:无法同时保持类型信息和参数数量的灵活性。

Session 从泛型和可变参数的基础概念出发,指出了当前系统的痛点。可变参数(variadic parameters)可以接受任意数量的参数,但只能处理单一类型,且无法保留每个参数的具体类型信息。想要同时做到”参数数量灵活”和”类型信息保留”,唯一的办法就是为每个参数数量写一个重载——这就是为什么很多 API 有 1 参数、2 参数、3 参数、4 参数版本的重复代码。

参数包通过引入 type pack(类型包)和 value pack(值包)的概念解决了这个问题。用 each 关键字声明类型参数包,用 repeat 关键字定义重复模式,编译器会在编译期展开这些包,生成类型安全的代码。

Session 用一个服务器查询 API 的例子贯穿始终,展示了参数包如何将 N 个重载函数精简为一个泛型实现。

值得深挖的点

类型包与值包的对应关系:类型包中的每个类型和值包中对应位置的值是一一对应的。比如类型包 (Bool, Int, String) 和值包 (true, 10, "") 在位置 0、1、2 上分别对应。这种对应关系在编译期就被确定,不存在运行时类型擦除。

repeat 关键字的展开机制repeat 后面跟一个包含 each 引用的模式。编译器会把模式对包中的每个元素重复一次,生成逗号分隔的列表。比如 repeat Request<each Payload> 展开为 Request<Bool>, Request<Int>, Request<String>。这就是为什么 repeat 只能用在天然接受逗号分隔列表的位置:元组、函数参数列表、泛型参数列表。

命名约定用单数each Payload 而不是 each Payloads。虽然 pack 包含多个元素,但 repeat 是逐个迭代的,每次迭代只处理一个元素,所以用单数更自然。

与集合的本质区别:集合中的元素是同类型的,通过 for-in 循环在运行时迭代。参数包中的每个元素有不同类型,通过 repeat 模式在编译期展开。这是类型级别的抽象,不是值级别的抽象。

代码片段

// 旧方式:为每个参数数量写重载(痛苦的重复代码)
func query<P1>(_ request: Request<P1>) -> P1
func query<P1, P2>(_ r1: Request<P1>, _ r2: Request<P2>) -> (P1, P2)
func query<P1, P2, P3>(_ r1: Request<P1>, _ r2: Request<P2>, _ r3: Request<P3>) -> (P1, P2, P3)
// 支持到第 4 个参数就要再写一遍...

// 新方式:用参数包一次性解决
func query<each Payload>(
    _ request: repeat Request<each Payload>
) -> (repeat each Payload) {
    return (repeat send(request: each request))
}
// 参数包的基本语法
// 1. 声明类型参数包
func process<each Item>(items: repeat each Item) {
    // repeat each Item 会对包中每个元素展开
}

// 2. 调用时传入任意数量的不同类型参数
process(items: true, 42, "hello")
// 编译器展开为:process(items: true, 42, "hello")
// 类型推导为:process<Bool, Int, String>(items: true, 42, "hello")
// 实际应用:类型安全的多元组构建
func makeTuple<each Element>(
    _ element: repeat each Element
) -> (repeat each Element) {
    return (repeat each element)
}

// 自动推导返回类型为 (Bool, Int, String)
let tuple = makeTuple(true, 42, "hello")

// 参数包也可以为空
let emptyTuple = makeTuple() // 返回类型为 ()

最佳实践

  • 识别重复重载模式:如果你的 API 有多个仅参数数量不同的重载,那就是使用参数包的信号。不要继续添加第 5 个、第 6 个重载。
  • 用单数命名参数包each Payload 而非 each Payloads,因为 repeat 每次迭代只处理一个元素。
  • 理解 repeat 的使用限制:repeat 只能用在逗号分隔列表的位置——元组类型、函数参数列表、泛型参数列表。不能在任意位置使用。
  • 优先让编译器推导类型:调用参数包函数时不需要显式指定泛型参数,编译器会根据传入的参数自动推导类型包。
  • 从使用侧理解:作为 API 调用者,你不需要理解参数包的内部机制。你只需要像调用普通函数一样传入不同数量和类型的参数,编译器会处理一切。

还有什么值得关注

  • 参数包是 Swift 泛型系统自引入以来最大的增强之一,它补上了”类型级可变抽象”这块拼图。
  • 这个特性直接影响了很多标准库 API 的设计,比如 SwiftUI 中可能出现的更灵活的 ViewBuilder。
  • Session 强调这是高级特性——如果你不经常写泛型库代码,可能不会直接用到。但理解它有助于阅读使用了参数包的库 API。
  • 参数包的展开发生在编译期,所以不会引入任何运行时开销。生成的代码和你手写重载完全一样高效。
  • 建议先看 WWDC22 的 “Embrace Swift generics” 打好基础,再来理解这场 Session 的内容。
WWDC 2023