Swift 一个小型游戏对象模型渐进式设计(四)——类型擦除与 Existential:当泛型遇见动态派发

为什么"泛型"还不够

上一篇我们写出了这样的代码:

swift 复制代码
let calc: any DamageCalculator<Double> = CritCalculator(rate: 1.5)

它编译得快、跑得也快,但当你想把它存进数组、或者作为属性逃逸到运行时,就会遇到三个灵魂问题:

  1. 编译器不知道具体类型有多大,如何分配内存?
  2. 协议里有 associatedtype,为什么不能用 DamageCalculator 直接当做类型?
  3. 同样一句 calculate(base:),为什么有时走内联、有时走虚表?

答案都指向同一个机制:Existential Container(存在性容器),社区俗称"类型擦除盒"。

Existential 是什么

Swift 把"符合某个协议的值"打包成一种统一大小的盒子,这个盒子就叫 existential。

语法层面:

  • any Protocol // Swift 5.6+ 显式 existential
  • 老代码里的 Protocol // 隐式 existential,即将被逐步废弃

盒子内部到底长什么样?继续看。

Existential Container 的内存布局

以 64 bit 为例,标准布局 5 个字(40 byte):

lua 复制代码
+-------- 0:  value buffer (3 ptr = 24 byte)  
+--------24:  value witness table (VWT)  
+--------32:  protocol witness table (PWT)  
  1. value buffer

    • 小值(Int、Double、CGPoint...)直接内联;
    • 大值(String、Array、自定义 class)堆分配,buffer 存指针;
  2. VWT

    管理"值语义"生命周期:拷贝、销毁、搬移。

  3. PWT

    管理"协议方法"派发地址,相当于 C++ 的 vtable。

结论:哪怕只是一个 Double,装进 any NumericValue 后也会膨胀到 40 字节;如果频繁在数组里拷贝,就会带来隐式堆分配和缓存抖动。

关联类型协议的"额外"盒子

当协议带 associatedtype 时,existential 还需要一份通用签名(generic environment),用于在运行时保存类型元数据。

因此:

swift 复制代码
let x: any Attackable        // ❌ 编译错误:associatedtype Value 未定
let y: any Attackable<Int>   // ✅ Swift 5.9 新语法:parameterized existential

后者内部比"无关联类型"再多 8 byte,总计 48 byte。

苹果在 WWDC23 给出的性能警告:< 3 个 witness 方法且 value ≤ 24 byte 时,existential 才基本无额外开销;否则请考虑"手写类型擦除"或"泛型特化"。

实战:手写 AnyDamageCalculator

目标:

  • 对外暴露固定大小(无动态盒子);
  • 对内保存任意具体计算器;
  • 仍保持 Value 泛型参数。
  1. 定义抽象基类(引用语义)
swift 复制代码
class AnyDamageCalculatorBox<Value: NumericValue>: DamageCalculator {
    func calculate(base: Value) -> Value { fatalError("abstract") }
}
  1. 定义具体盒子(泛型类)
swift 复制代码
final class ConcreteBox<T: DamageCalculator>: AnyDamageCalculatorBox<T.Value> {
    private let concrete: T
    init(_ concrete: T) { self.concrete = concrete }
    override func calculate(base: Value) -> Value {
        concrete.calculate(base: base)
    }
}
  1. 定义值包装(对外类型)
swift 复制代码
struct AnyDamageCalculator<Value: NumericValue>: DamageCalculator {
    private let box: AnyDamageCalculatorBox<Value>
    
    init<C: DamageCalculator>(_ concrete: C) where C.Value == Value {
        self.box = ConcreteBox(concrete)
    }
    
    func calculate(base: Value) -> Value {
        box.calculate(base: base)
    }
}
  1. 使用:
swift 复制代码
let crit = CritCalculator(rate: 1.5)
let erased: AnyDamageCalculator<Double> = AnyDamageCalculator(crit)
array.append(erased)   // 数组元素大小 = 1 ptr,无 existential 盒子
  • 内存大小:8 byte(一个 class 指针);
  • 拷贝成本:一次 ARC retain;
  • 方法派发:虚表一次,但不再额外带 VWT/PWT。

Swift 5.9 新武器:Parameterized Existential

swift 复制代码
let list: [any DamageCalculator<Double>] = [
    CritCalculator(rate: 1.5),
    MultiplierCalculator(upstream: CritCalculator(rate: 2), multiplier: 1.2)
]

编译器会自动生成"隐藏盒子",但仍带 48 byte 拷贝成本。

适合场景:

  • 原型阶段、快速迭代;
  • 对性能不敏感的工具代码;

高性能路径(渲染、音频、网络解析)继续用手写擦除或泛型特化。

类型擦除的通用套路(模板)

任何带 associatedtype 的协议,都可以套下面 4 步:

  1. 创建 AnyProtocolBase<AssociatedType> 抽象类;
  2. 创建 ConcreteBox<T: Protocol> 具体类,持有 T
  3. 创建 AnyProtocol<AssociatedType> 值类型,内部存 AnyProtocolBase 指针;
  4. 对外 API 全部 override / forward 到抽象类。

什么时候用哪种形态?

markdown 复制代码
需求 \ 方案        泛型特化   any Protocol   手写擦除
------------------------------------------------------------
编译期已知类型       ✅          ❌             ❌
需要进数组/逃逸      ❌          ✅             ✅
对性能极度敏感       ✅          ❌             ✅
不想写样板代码       ✅          ✅             ❌(可用宏)

一句话:编译期能定类型就用泛型;运行时再决定就用擦除;原型阶段先 any 再说。

相关推荐
报错小能手2 天前
ios开发方向——swift并发进阶核心 Task、Actor、await 详解
开发语言·学习·ios·swift
用户79457223954133 天前
【AFNetworking】OC 时代网络请求事实标准,Alamofire 的前身
objective-c·swift
报错小能手3 天前
SwiftUI 框架 认识 SwiftUI 视图结构 + 布局
ui·ios·swift
东坡肘子3 天前
被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131
人工智能·swiftui·swift
报错小能手4 天前
ios开发方向——swift错误处理:do/try/catch、Result、throws
开发语言·学习·ios·swift
小夏子_riotous4 天前
openstack的使用——5. Swift服务的基本使用
linux·运维·开发语言·分布式·云计算·openstack·swift
mCell4 天前
MacOS 下实现 AI 操控电脑(Computer Use)的思考
macos·agent·swift
用户79457223954134 天前
【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线
swiftui·swift
chaoguo12345 天前
Any metadata 的内存布局
swift·metadata·value witness table
tangweiguo030519876 天前
SwiftUI布局完全指南:从入门到精通
ios·swift