Swift 的协议一旦带上 associatedtype,就像给类型贴了一张"待填写的支票"------编译时必须知道具体填什么数字,否则无法兑现。
这导致一个经典编译错误:
swift
protocol Shape {
associatedtype Parameters
func area(with parameters: Parameters) -> Double
}
struct Circle: Shape {
struct CircleParameters {
let radius: Double
}
func area(with parameters: CircleParameters) -> Double {
return Double.pi * parameters.radius * parameters.radius
}
}
struct Rectangle: Shape {
struct RectangleParameters {
let width: Double
let height: Double
}
func area(with parameters: RectangleParameters) -> Double {
return parameters.width * parameters.height
}
}
struct Triangle: Shape {
// 定义三角形面积计算所需的参数结构体
struct TriangleParameters {
let base: Double // 底边长
let height: Double // 对应底边的高
}
// 实现Shape协议的area方法,使用底乘高除以2计算面积
func area(with parameters: TriangleParameters) -> Double {
return (parameters.base * parameters.height) / 2.0
}
}
// Use of protocol 'Shape' as a type must be written 'any Shape'; this will be an error in a future Swift language mode
// 目前只是警告,后续会演变成错误
let shapes: [Shape] = [Circle(), Rectangle()]
existentials(any Protocol
)与 Swift 5.7 引入的 primary associated type 正是为了解决"既要协议约束,又要异质容器"的两难问题。
基础概念速览
术语 | 一句话解释 | 本文代号 |
---|---|---|
Protocol with associated type (PAT) | 带关联类型的协议 | Shape |
Existential(存在量词类型) | 编译期"橡皮擦",把具体类型擦成盒子 | any Shape |
Primary associated type | 给协议额外贴一个"标签",在 any 盒子外再写关联类型 |
Shape<CircleParameters> |
Type erasure | 把不同具体类型抹平成同一盒子,代价是丢失静态信息 | 下文详解 |
业务场景:统一计算任意图形的面积
定义协议(含关联类型)
swift
protocol Shape {
/// 每个图形需要的参数不一样,用关联类型抽象
associatedtype Parameters
/// 根据外部传入的参数计算面积
func area(with parameters: Parameters) -> Double
}
具体实现:圆与矩形
swift
// MARK: - Circle
struct Circle: Shape {
struct CircleParameters {
let radius: Double
}
func area(with parameters: CircleParameters) -> Double {
Double.pi * parameters.radius * parameters.radius
}
}
// MARK: - Rectangle
struct Rectangle: Shape {
struct RectangleParameters {
let width: Double
let height: Double
}
func area(with parameters: RectangleParameters) -> Double {
parameters.width * parameters.height
}
}
异质容器:把圆、矩形、三角形放一起
swift
// ❌ 直接写 [Shape] 会报警告
// var shapes: [Shape] = [Circle(), Rectangle()]
// ✅ 使用 existential(类型擦除盒子)
var shapes: [any Shape] = [Circle(), Rectangle()]
注意:
any Shape
是 Swift 5.6+ 的显式语法;老版本可省略any
,但 Xcode 14 起会警告。
运行期"拆盒子"------向下转型
类型被擦除后,编译器不知道盒子里的真实类型,只能手动拆:
swift
let circleParams = Circle.CircleParameters(radius: 10)
let rectangleParams = Rectangle.RectangleParameters(width: 5, height: 8)
for shape in shapes {
if let circle = shape as? Circle {
print("Circle area: \(circle.area(with: circleParams))")
} else if let rectangle = shape as? Rectangle {
print("Rectangle area: \(rectangle.area(with: rectangleParams))")
}
}
缺点
- 运行时转型,错一个字母就崩溃。
- 每新增一个图形都要改
if/else
。 - 无法利用泛型静态派发,失去性能优势。
Primary associated type:给协议加"标签"
Swift 5.7 允许在协议名后直接把关联类型"提"到尖括号里,成为 primary associated type。
升级协议
swift
protocol Shape<Parameters> {
associatedtype Parameters
func area(with parameters: Parameters) -> Double
}
语法糖等价于:
swift
protocol Shape {
associatedtype Parameters
func area(with parameters: Parameters) -> Double
}
差别在于:调用方现在可以显式写 any Shape<CircleParameters>
,把擦除粒度变细。
同质容器:不再瞎猜类型
swift
// 只接受 Circle 的盒子
let circles: [any Shape<Circle.CircleParameters>] = [Circle(), Circle()]
for circle in circles {
// 无需转型,编译器已知 Parameters == CircleParameters
print(circle.area(with: .init(radius: 3)))
}
异质容器:回到 any Shape
swift
// 仍然可以擦除到底
let mixed: [any Shape] = [Circle(), Rectangle()]
primary associated type 并没有破坏"擦除"能力,只是让你有机会在需要细粒度时把类型信息拉回来。
对比总结:何时用谁?
场景 | 推荐写法 | 转型成本 | 新增类型成本 |
---|---|---|---|
同质集合(全是 Circle) | [any Shape<CircleParameters>] |
0 | 低 |
异质集合(圆+矩形) | [any Shape] + 转型 |
高 | 高(要改 if/else) |
性能敏感 & 静态派发 | 用泛型 func draw<T: Shape>(_ shape: T) |
0 | 0 |
可落地的扩展场景
网络层抽象
swift
protocol Request<Response> {
associatedtype Response: Decodable
var url: URL { get }
}
class User: Decodable {}
struct GetUsers: Request {
typealias Response = [User]
var url: URL {
URL(string: "")!
}
}
class SearchUsers: Request {
typealias Response = [User]
let query: String
init(query: String) {
self.query = query
}
var url: URL {
URL(string: "")!
}
}
let endpoints: [any Request<[User]>] = [GetUsers(), SearchUsers(query: "Swifty")]
数据库 DAO
swift
protocol PersistentModel {}
protocol DAO<Entity> {
associatedtype Entity: PersistentModel
func insert(_ e: Entity) throws
}
struct User: PersistentModel {}
struct UserDAO: DAO {
func insert(_ e: User) throws {
}
}
// 只操作用户表
let userDAO: any DAO<User> = UserDAO()
SwiftUI 的 View & Reducer
利用 any Store<State, Action>
在预览时注入 mock,生产环境注入真实 store,一套代码两端复用。
最佳实践
-
优先泛型,次选 existential
泛型函数/类型在编译期就能确定具体类型,零成本抽象;existentials 是运行期盒子,有轻微内存与派发开销。
-
primary associated type 不是银弹
它只能把"关联类型"提到签名里,不能把
Self
提出来。若协议里出现func f(_: Self)
,仍然无法消除转型。 -
用 typealias 降低视觉噪音
swift
typealias AnyCircleShape = any Shape<Circle.CircleParameters>
-
大型项目给 existential 写单元测试
转型分支容易遗漏,用 XCTest 参数化遍历所有 conforming type,确保
area
计算正确。
一句话收束
existentials 像"橡皮擦",让不同类型共处一室;primary associated type 像"标签",让擦除后仍保留关键线索。
掌握这对组合拳,你就能在"灵活"与"性能"之间自由切换,写出既 Swifty 又高效的代码。