Design protocol interfaces in Swift
Design 进阶 20m

在 Swift 中设计协议接口

Design protocol interfaces in Swift

2022年6月6日

在 Apple 官方观看视频

一句话判断

这个 Session 解决的是 Swift 协议设计中一个长期的痛点——associated type 的类型擦除、不透明返回类型的约束,以及如何用 same-type requirement 精确建模类型关系。

这场 Session 讲了什么

Swift 的 protocol 很强大,但在实际使用中总会碰到一些”明明应该能编译但就是不通过”的情况。这个 Session 系统地讲解了三个关键的协议设计模式。

Associated Type 的类型擦除。 当一个协议有 associated type 时,你无法直接用 any MyProtocol 作为类型——因为编译器不知道 associated type 的具体类型是什么。Session 讲了如何用 existential type(any)配合泛型约束来解决这个问题。Swift 5.7 大幅改进了 any 的能力,很多以前需要手写 AnyXxx 类型擦除包装器的场景现在直接用 any 就行。

带约束的不透明返回类型(Constrained opaque result types)。 some 关键字在 Swift 5.7 中获得了 primary associated types 的支持。你可以写 some Collection<Int> 来表达”返回某种 Collection,但元素类型确定是 Int”。这让协议的返回值类型更精确,同时保留了实现的灵活性。

Same-type requirement。 这是协议设计中最容易被忽视的工具。通过 where AssociatedType == ConcreteType,你可以在协议扩展中精确约束 associated type 的具体类型,从而提供更特化的默认实现。这对于建模类型之间的关系特别有用——比如”Producer 生产的 Product 必须和 Consumer 消费的 Product 是同一种类型”。

值得深挖的点

Primary associated types 改变了泛型的表达方式。 以前你写 some Publisher,调用者完全不知道输出的数据类型。现在有了 some Publisher<Int, Never>,类型信息一目了然。这和 Array<Int>Collection<String> 的语法保持了一致——类型参数直接写在尖括号里。苹果自己的框架(Combine、SwiftUI)已经在大量使用这个特性。

类型擦除的成本不应该被忽视。 any 虽然方便,但它确实引入了间接调用的开销。Session 的建议是:在公共 API 边界用 some 表达”某个确定的类型”,在需要存储异构集合时用 any,在不需要类型擦除的时候用泛型参数。三种工具有不同的适用场景,不要一股脑都用 any

代码片段

// Primary associated types 让协议更精确
protocol Producer {
    associatedtype Product
    func produce() -> Product
}

protocol Consumer {
    associatedtype Product
    func consume(_ item: Product)
}

// Same-type requirement 建模类型关系
func process<P: Producer & Consumer>(producer: P)
    where P.Product == P.Product
{
    let item = producer.produce()
    producer.consume(item)
}

// 使用 some 表达不透明返回类型
func makeProducer() -> some Producer<Int> {
    NumberProducer()
}

// Swift 5.7 之前需要手写类型擦除
// 现在直接用 any
let producers: [any Producer<Int>] = [
    NumberProducer(),
    AnotherProducer()
]

// 实际的协议设计示例
protocol DataSource {
    associatedtype Item: Equatable
    associatedtype Error: Swift.Error

    var itemCount: Int { get }
    func item(at index: Int) -> Item
    func reload() async throws -> [Item]
}

最佳实践

  • 在协议定义中为 associated type 添加合适的约束(比如 associatedtype Element: Equatable),这能让编译器为你做更多检查。
  • 优先用 some 表达返回值的类型约束,只在确实需要异构集合时才用 any
  • 使用 same-type requirement 来建模协议组合中的类型关系,避免运行时类型转换。
  • 在设计公共 API 时,考虑哪些 associated type 应该声明为 primary——它们会出现在尖括号里,是调用者最关心的类型参数。

还有什么值得关注

  • anysome 的区别不仅是语义上的——some 保证底层只有一个具体类型,any 允许运行时变化。
  • Swift 5.7 对 any 的改进是渐进式的,未来版本可能会进一步简化 existential type 的使用。
  • 标准库中的 Collection<Element>Publisher<Output, Failure> 都采用了 primary associated types 的设计。
  • 如果你维护的库有手写的类型擦除包装器(比如 AnyPublisherAnyView),可以评估是否能在新版本中用 any 替代。
WWDC 2022