Swift 里的“橡皮擦”与“标签”——搞懂 existentials 与 primary associated type

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))")
    }
}

缺点

  1. 运行时转型,错一个字母就崩溃。
  2. 每新增一个图形都要改 if/else。
  3. 无法利用泛型静态派发,失去性能优势。

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,一套代码两端复用。

最佳实践

  1. 优先泛型,次选 existential

    泛型函数/类型在编译期就能确定具体类型,零成本抽象;existentials 是运行期盒子,有轻微内存与派发开销。

  2. primary associated type 不是银弹

    它只能把"关联类型"提到签名里,不能把 Self 提出来。若协议里出现 func f(_: Self),仍然无法消除转型。

  3. 用 typealias 降低视觉噪音

swift 复制代码
typealias AnyCircleShape = any Shape<Circle.CircleParameters>
  1. 大型项目给 existential 写单元测试

    转型分支容易遗漏,用 XCTest 参数化遍历所有 conforming type,确保 area 计算正确。

一句话收束

existentials 像"橡皮擦",让不同类型共处一室;primary associated type 像"标签",让擦除后仍保留关键线索。

掌握这对组合拳,你就能在"灵活"与"性能"之间自由切换,写出既 Swifty 又高效的代码。

相关推荐
东坡肘子25 分钟前
当 Build in Public 重新转入地下 -- 肘子的 Swift 周报 #157
人工智能·swiftui·swift
用户380341658829717 小时前
Flutter 迁 SwiftUI:AI 把页面"搬"过来只用了两周,我们补结构花了两个月
ios·swift
茶底世界之下20 小时前
四角变换后的最小包围视口,为什么不能只把宽高取整?
ios·swift
茶底世界之下2 天前
预乘 Alpha 图层合成为什么会越叠越暗?
架构·swift
茶底世界之下4 天前
时间线效果层,为什么不能只复用一个处理器?
ios·swift
Cc.Y4 天前
Swift深度链接的极简之道
ios·cocoa·swift
传奇开心果编程4 天前
【SwiftUI提高练中学】第9课 安全与隐私
swiftui·移动开发·swift·编程语言理论·开发范式·编程框架·声明式ui
茶底世界之下5 天前
动态切换 nearest / linear 时,为什么不能只替换 sampler
ios·swift
应用市场5 天前
把旧 Pixel 变成相册备份中转站(上):Mac 到安卓的照片传输工具设计——流式上传、sha256 校验、adb forward 与 Bonjour
android·macos·adb·kotlin·swift
东坡肘子5 天前
Swift Server,又多了一个 Google -- 肘子的 Swift 周报 #156
人工智能·swiftui·swift