如何定义存储非指针类型的Swift结构体且兼顾易测试性?
最优解决方案:泛型+协议约束
你可以通过带协议约束的泛型实现需求,既完全避免协议 existential 容器带来的指针存储开销,内部直接存储值类型,又能保留完整的可测试性,不需要引入编译标记的脏逻辑。
完整实现代码如下:
protocol TimeGetable { var currentTime: Date { get } } // 用泛型约束替代existential协议类型作为属性类型 struct Chronometer<T: TimeGetable> { private let timeGetter: T private var startTime: Date init(timeGetter: T) { self.timeGetter = timeGetter startTime = timeGetter.currentTime } mutating func reset() { startTime = timeGetter.currentTime } func elapsedTime() -> TimeInterval { timeGetter.currentTime.timeIntervalSince(startTime) } } // 生产环境用的时间获取实现 struct RealGetter: TimeGetable { var currentTime: Date { Date() } } // 测试环境用的Mock实现 struct FakeGetter: TimeGetable { var currentTime: Date { // 可自定义测试逻辑 return Date.distantPast } }
生产环境便利优化
为了让生产代码的用法和你写死Date的版本完全一致,不需要显式传入时间获取器,可以给Chronometer加一个针对RealGetter的扩展初始化器:
extension Chronometer where T == RealGetter { init() { self.init(timeGetter: RealGetter()) } }
用法说明
- 生产代码直接调用
Chronometer()初始化即可,和你无测试版本的用法完全没有区别 - 测试代码显式注入Mock实现:
let testChronometer = Chronometer(timeGetter: FakeGetter())
方案优势
- 纯值类型存储,无额外开销:泛型走静态派发,编译器会为每个具体的TimeGetable实现生成专用的Chronometer变体,timeGetter属性直接作为值类型存储在结构体的内存布局中,没有existential协议的指针开销,性能和你直接写死Date的版本完全一致,不会出现缓存失效问题。
- 无测试代码污染生产环境:不需要编译分支标记,Mock类型仅在测试target中定义即可,生产代码完全不会感知到测试相关逻辑的存在。
- 内存占用符合预期:当使用无存储属性的RealGetter时,
MemoryLayout<Chronometer<RealGetter>>.size的大小就是两个Date的大小之和,比你之前使用existential协议的48字节版本更小,内存占用和你纯手写无测试的版本完全一致。
内容的提问来源于stack exchange,提问作者Mateusz Stompór
相关产品推荐
相关产品推荐

