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 又高效的代码。