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 再说。

相关推荐
HarderCoder2 小时前
Swift 一个小型游戏对象模型渐进式设计(五)——Swift 并发世界:把 Attackable 搬进 actor
swift
HarderCoder2 小时前
Swift 一个小型游戏对象模型渐进式设计(三)——把能力再抽象一层,写一套“伤害计算器”框架
swift
HarderCoder2 小时前
Swift 一个小型游戏对象模型渐进式设计(二)——协议与默认实现:如何写出不用继承的多态
swift
HarderCoder3 小时前
Swift 一个小型游戏对象模型渐进式设计(一)——继承机制解读:从基础类到防止重写
swift
HarderCoder4 小时前
Swift 中的迭代机制:Sequence、Collection 与 Iterator 完全拆解
swift
HarderCoder9 小时前
告别并发警告:Swift 6 线程安全通知 MainActorMessage & AsyncMessage 实战指南
swift
HarderCoder9 小时前
【SwiftUI 任务身份】task(id:) 如何正确响应依赖变化
swift
非专业程序员10 小时前
精读GitHub - swift-markdown-ui
ios·swiftui·swift
5***79001 天前
Swift进阶
开发语言·ios·swift