Highlight
Swift 5.9 引入参数包(Parameter Packs),用
each声明类型参数包、repeat定义重复模式,让单个泛型函数同时抽象参数数量和参数类型,彻底消除 N 重函数重载的样板代码。
核心内容
Swift 的泛型系统已经能很好地抽象类型。Array<Element> 可以容纳任何类型,query<Payload>(_ item: Request<Payload>) -> Payload 可以处理任意请求类型。
但当你需要同时发起多个请求时,问题出现了。每个请求可能返回不同类型,你希望结果是一个元组,每个位置的类型对应请求的类型。
(02:04)
重载地狱
目前的做法是写多个重载:
func query<Payload>(_ item: Request<Payload>) -> Payload
func query<Payload1, Payload2>(
_ item1: Request<Payload1>,
_ item2: Request<Payload2>
) -> (Payload1, Payload2)
func query<Payload1, Payload2, Payload3>(
_ item1: Request<Payload1>,
_ item2: Request<Payload2>,
_ item3: Request<Payload3>
) -> (Payload1, Payload2, Payload3)
func query<Payload1, Payload2, Payload3, Payload4>(
_ item1: Request<Payload1>,
_ item2: Request<Payload2>,
_ item3: Request<Payload3>,
_ item4: Request<Payload4>
) -> (Payload1, Payload2, Payload3, Payload4)
四个重载,代码几乎一样。传五个参数就编译报错。更糟的是,这种重载模式在 Swift 标准库和第三方库中无处不在。
(03:23)
参数包的概念
参数包引入了两个新概念:
类型包(type pack)可以容纳任意数量的类型。比如一个类型包可以包含 Bool、Int、String 三个独立类型。
值包(value pack)可以容纳任意数量的值。比如一个值包可以包含 true、10、"" 三个独立值。
类型包和值包配合使用:类型包中的每个类型对应值包中同位置的值。位置 0 的 true 类型是 Bool,位置 1 的 10 类型是 Int,位置 2 的 "" 类型是 String。
(04:41)
用参数包重写 query
用参数包后,N 个重载压缩成一个函数:
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
关键点:
each Payload声明一个类型参数包repeat Request<each Payload>表示参数列表中重复出现Request<each Payload>repeat each Payload表示返回元组中重复出现each Payload
调用时完全自然:
let result = query(Request<Int>())
// result 类型是 Int
let results = query(Request<Int>(), Request<String>(), Request<Bool>())
// results 类型是 (Int, String, Bool)
编译器在编译期展开参数包,生成类型安全的代码。你不需要写任何重载。
(09:37)
详细内容
参数包的语法规则
声明类型参数包用 each 关键字:
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
repeat 只能在逗号分隔的列表中使用:元组类型、函数参数列表、泛型参数列表。不能在任意位置使用。
可以给参数包添加约束:
// 所有类型都遵循 Equatable
func query<each Payload: Equatable>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
// 或者用 where 子句
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
where repeat each Payload: Equatable
也可以混合普通泛型参数和参数包:
func query<FirstPayload, each Payload>(
_ first: Request<FirstPayload>,
_ item: repeat Request<each Payload>
) -> (FirstPayload, repeat each Payload)
where FirstPayload: Equatable, repeat each Payload: Equatable
(11:03)
参数包的实现
参数包函数的实现用 repeat 来迭代处理每个元素:
struct Request<Payload> {
func evaluate() -> Payload
}
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload) {
return (repeat (each item).evaluate())
}
关键点:
each item解包值包中的每个元素(each item).evaluate()对每个元素调用方法repeat (...)把结果打包成值包- 外层括号
(repeat ...)把值包组装成元组
(13:42)
更复杂的例子
当输入和输出类型不同时,参数包同样适用:
protocol RequestProtocol {
associatedtype Input
associatedtype Output
func evaluate(_ input: Input) -> Output
}
struct Evaluator<each Request: RequestProtocol> {
var item: (repeat each Request)
func query(
_ input: repeat (each Request).Input
) -> (repeat (each Request).Output) {
return (repeat (each item).evaluate(each input))
}
}
这里:
each Request是类型参数包,每个类型都遵循RequestProtocol(each Request).Input获取每个请求类型的 Input 关联类型(each Request).Output获取每个请求类型的 Output 关联类型each input和each item分别解包输入值包和请求值包(each item).evaluate(each input)把对应的输入传给对应的请求
(16:04)
带控制流的参数包
参数包也可以和 try 配合使用:
protocol RequestProtocol {
associatedtype Input
associatedtype Output
func evaluate(_ input: Input) throws -> Output
}
struct Evaluator<each Request: RequestProtocol> {
var item: (repeat each Request)
func query(
_ input: repeat (each Request).Input
) -> (repeat (each Request).Output)? {
do {
return (repeat try (each item).evaluate(each input))
} catch {
return nil
}
}
}
repeat try (...) 表示对每个元素都应用 try。如果任何一个请求抛出错误,整个 do 块进入 catch。
(17:05)
核心启发
重构你的网络层 API
如果你的项目有多个 fetch 方法的重载(1 参数、2 参数、3 参数…),用参数包合并成一个。调用方体验不变,维护成本大幅降低。
入口:把 func fetch<T1, T2>(_ r1: Request<T1>, _ r2: Request<T2>) -> (T1, T2) 等重载替换为 func fetch<each T>(_ request: repeat Request<each T>) -> (repeat each T)。
类型安全的依赖注入容器
用参数包实现一个 Container.resolve() 方法,同时解析多个依赖:
let (db, network, cache) = container.resolve(Database.self, Network.self, Cache.self)
入口:定义 func resolve<each T>() -> (repeat each T),容器内部根据类型查找对应的实例。
多路并发请求的统一封装
在 SwiftUI 或 Combine 项目中,常有同时发起多个请求、等全部完成后再更新 UI 的场景。用参数包封装一个 concurrent 函数:
let (user, posts, settings) = await concurrent(
fetchUser(),
fetchPosts(),
fetchSettings()
)
入口:func concurrent<each T>(_ task: repeat @Sendable () async -> each T) async -> (repeat each T),内部用 withTaskGroup 并发执行。
元组类型的泛型工具
写一个 zip 函数,把多个 Optional 打包成一个 Optional 元组:
let result = zip(opt1, opt2, opt3)
// result: (T1, T2, T3)?
入口:func zip<each T>(_ value: repeat each T?) -> (repeat each T)?,当所有值都非 nil 时返回元组,否则返回 nil。
关联 Session
- Write Swift macros — 动手编写第一个 Swift 宏
- Expand on Swift macros — 深入理解宏的设计原则和更多角色类型
- Swift concurrency: Behind the scenes — Swift 结构化并发的底层原理
- Meet Swift OpenAPI Generator — 用代码生成简化服务端 API 调用
评论
GitHub Issues · utterances