You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何定义存储非指针类型的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())

方案优势

  1. 纯值类型存储,无额外开销:泛型走静态派发,编译器会为每个具体的TimeGetable实现生成专用的Chronometer变体,timeGetter属性直接作为值类型存储在结构体的内存布局中,没有existential协议的指针开销,性能和你直接写死Date的版本完全一致,不会出现缓存失效问题。
  2. 无测试代码污染生产环境:不需要编译分支标记,Mock类型仅在测试target中定义即可,生产代码完全不会感知到测试相关逻辑的存在。
  3. 内存占用符合预期:当使用无存储属性的RealGetter时,MemoryLayout<Chronometer<RealGetter>>.size的大小就是两个Date的大小之和,比你之前使用existential协议的48字节版本更小,内存占用和你纯手写无测试的版本完全一致。

内容的提问来源于stack exchange,提问作者Mateusz Stompór

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 14:15:03