WWDC Quick Look 💓 By SwiftGGTeam
Generalize APIs with parameter packs

Generalize APIs with parameter packs

观看原视频

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)可以容纳任意数量的类型。比如一个类型包可以包含 BoolIntString 三个独立类型。

值包(value pack)可以容纳任意数量的值。比如一个值包可以包含 true10"" 三个独立值。

类型包和值包配合使用:类型包中的每个类型对应值包中同位置的值。位置 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 inputeach 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

评论

GitHub Issues · utterances