为什么要"再抽象一层"
上两篇我们已经用协议把"攻击"拆成了能力插件,但遗留了一个硬核问题:
- 游戏前期用
Int足够,后期为了避免除法误差想换成Double,甚至金融级精度要用Decimal; - 如果给每种数值类型都复制一份协议,就会出现
AttackableInt、AttackableDouble...爆炸式增长。
Swift 的泛型(Generic)+ 关联类型(associatedtype)可以"一次性"写出算法,然后让编译器在调用点自动生成对应版本的代码,既保证类型安全,又保持运行时零成本。
把 Attackable 升级成泛型协议
- 定义"数值"契约
先约定一个"可运算、可比较"的基本协议,把 +、*、/、> 等运算符包进去:
swift
protocol NumericValue: Comparable {
static func + (lhs: Self, rhs: Self) -> Self
static func * (lhs: Self, rhs: Self) -> Self
static func / (lhs: Self, rhs: Self) -> Self
static func > (lhs: Self, rhs: Self) -> Bool // 与标量乘
init(_ value: Int) // 能从整数字面量初始化
}
- 让标准库类型自动符合
Swift 5.7 之后可以用 extension 给标准库类型"批量"实现:
swift
extension Int: NumericValue {}
extension Double: NumericValue {}
extension Decimal: NumericValue {
static func *(lhs: Decimal, rhs: Double) -> Decimal {
lhs * Decimal(rhs)
}
}
(Float、CGFloat 同理)
- 泛型版 Attackable
swift
protocol Attackable {
associatedtype Value: NumericValue // ① 关联类型
func attack() -> Value
}
注意:
① 这里不能再给 attack() 提供默认实现,因为返回类型是泛型,不同数值的"默认伤害"语义不同;
② 如果确实想提供默认,可以再包一层泛型扩展
给"默认伤害"一个泛型实现
利用协议扩展的"where 子句"只对特定数值生效:
swift
extension Attackable where Value == Double {
func attack() -> Value { 10.0 }
}
extension Attackable where Value == Int {
func attack() -> Value { 10 }
}
extension Attackable where Value == Decimal {
func attack() -> Value { Decimal(10) }
}
这样任何符合者只要 Value 是上述三种之一,不实现 attack() 也能编译通过;想定制就再写一遍覆盖即可。
把"伤害计算器"也做成泛型组件
需求:
- 支持"暴击"、"易伤"、"免伤"多层修正;
- 算法写一次,对
Int / Double / Decimal全部生效; - 编译期决定类型,无运行时派发。
- 定义计算器协议
swift
protocol DamageCalculator<Value> {
associatedtype Value: NumericValue
/// 传入基础伤害,返回最终伤害
func calculate(base: Value) -> Value
}
- 默认实现:暴击 * 1.5
swift
struct CritCalculator<Value: NumericValue>: DamageCalculator {
let rate: Value // 暴击倍率
func calculate(base: Value) -> Value {
base * rate
}
}
- 链式组合:装饰器模式
swift
struct MultiplierCalculator<Value: NumericValue>: DamageCalculator {
let upstream: any DamageCalculator<Value> // 上游计算器
let multiplier: Double
func calculate(base: Value) -> Value {
let upstreamDamage = upstream.calculate(base: base)
return upstreamDamage * multiplier
}
}
使用:
swift
let crit: any DamageCalculator<Double> = CritCalculator(rate: 1.5)
let vulnerable = MultiplierCalculator(upstream: crit, multiplier: 1.2) // 易伤 +20%
let final = vulnerable.calculate(base: 100) // 100 * 1.5 * 1.2 = 180.0
把计算器塞进实体------"能力注入"
我们不再让实体"继承"伤害逻辑,而是把计算器当成属性注入:
swift
struct Warrior<Value: NumericValue>: Attackable {
let calculator: any DamageCalculator<Value>
func attack() -> Value {
let base: Value = Value(50) // 自己定基础值
return calculator.calculate(base: base)
}
}
使用:
swift
let warriorD = Warrior<Double>(calculator: vulnerable)
print(warriorD.attack()) // 90.0
一个文件里同时玩三种精度
swift
let wInt = Warrior<Int>(calculator: CritCalculator(rate: 2))
let wDouble = Warrior<Double>(calculator: CritCalculator(rate: 2))
let wDec = Warrior<Decimal>(calculator: CritCalculator(rate: 2))
print(wInt.attack()) // 100
print(wDouble.attack()) // 100.0
print(wDec.attack()) // 100
同一套算法,编译器自动生成三份特化(specialization)代码,运行时无盒子、无动态派发。
性能实测:零开销承诺是否兑现?
测试环境:M1 Mac / Swift 5.9 / -O 优化
swift
let p = Warrior<Double>(calculator: CritCalculator(rate: 1.8))
measure {
for _ in 0..<1_000_000 { _ = p.attack() }
}
结果:
- 泛型特化版本:0.047 s
- 手写
Double专用版本:0.046 s
差距在 2% 以内,属于测量误差;汇编层面已无线程堆分配、无 protocol witness 调用。
什么时候回到引用语义?
- 计算器需要状态缓存(如随机种子、CD 计时)且要共享;
- 需要继承 NSObjec 以兼容 KVO / Core Data;
- 需要互斥锁、原子引用计数。
其余场景继续 struct + 泛型协议。
最终决策清单(速查表)
| 需求场景 | 首选方案 | 备选方案 |
|---|---|---|
| 只是多态 | protocol 默认实现 | class + override |
| 多精度算法 | 泛型 protocol + associatedtype | 宏/模板代码生成 |
| 共享可变状态 | class | actor |
| 值语义 + 组合 | struct + protocol | 无 |
| 运行时动态替换 | class + objc | SwiftUI 的 AnyView 类型擦除 |