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

Swift带关联类型协议继承:如何实现非泛型ViewModel?

解决带关联类型协议的ViewModel依赖注入问题

问题场景

我们先定义了带关联类型的抽象业务用例协议:

protocol UseCase {
    associatedtype P
    associatedtype R
    func execute(params: P) async throws -> R
}

接着创建了约束参数和返回值类型的具体用例协议:

protocol TestUseCase: UseCase where P == Int, R == Int { }

并实现了该协议的具体类:

class TestUseCaseImpl: TestUseCase {
    func execute(params: Int) async throws -> Int {
        // 业务逻辑实现
        return params * 2
    }
}

但在ViewModel中直接声明TestUseCase类型的属性时,会触发编译错误:

class ViewModel {
    private let testUseCase: TestUseCase // 错误
    init(testUseCase: TestUseCase) { // 错误
        self.testUseCase = testUseCase
    }
}

错误提示:

Protocol 'TestUseCase' can only be used as a generic constraint because it has Self or associated type requirements

虽然泛型ViewModel可以解决单个用例的问题,但如果ViewModel需要依赖多个不同类型的用例,泛型参数会变得臃肿难以维护。以下是不使用泛型ViewModel的三种解决方案:

方案1:使用Swift 5.7+存在类型(any关键字)

Swift 5.7引入的存在类型语法,允许直接将带关联类型的协议作为具体类型使用,只需在协议前添加any关键字:

class ViewModel {
    private let testUseCase: any TestUseCase
    init(testUseCase: any TestUseCase) {
        self.testUseCase = testUseCase
    }
    
    func executeUseCase() async throws {
        let result = try await testUseCase.execute(params: 10)
        print("执行结果:\(result)")
    }
}

这个方案无需额外代码,是Swift 5.7及以上版本的最优解。

方案2:类型擦除(Type Erasure)

如果项目使用的Swift版本低于5.7,可以通过创建类型擦除的包装类,封装协议的行为:

// 类型擦除包装类
class AnyTestUseCase: TestUseCase {
    private let executeHandler: (Int) async throws -> Int
    
    init<U: TestUseCase>(_ useCase: U) {
        self.executeHandler = useCase.execute
    }
    
    func execute(params: Int) async throws -> Int {
        try await executeHandler(params)
    }
}

在ViewModel中使用这个包装类作为属性类型:

class ViewModel {
    private let testUseCase: AnyTestUseCase
    init(testUseCase: AnyTestUseCase) {
        self.testUseCase = testUseCase
    }
    
    func runBusinessLogic() async throws {
        let result = try await testUseCase.execute(params: 5)
        print("业务逻辑执行结果:\(result)")
    }
}

// 注入实例时的用法
let testImpl = TestUseCaseImpl()
let viewModel = ViewModel(testUseCase: AnyTestUseCase(testImpl))

方案3:重新定义无关联类型的协议

由于TestUseCase已经明确约束了参数和返回值为Int,可以直接定义一个不带关联类型的协议,替代原有的约束协议:

// 直接定义无关联类型的业务用例协议
protocol TestUseCase {
    func execute(params: Int) async throws -> Int
}

// 若需要复用原UseCase协议的逻辑,可让TestUseCase扩展实现它
protocol UseCase {
    associatedtype P
    associatedtype R
    func execute(params: P) async throws -> R
}

extension TestUseCase: UseCase {
    typealias P = Int
    typealias R = Int
}

// 实现类保持不变
class TestUseCaseImpl: TestUseCase {
    func execute(params: Int) async throws -> Int {
        return params * 2
    }
}

此时ViewModel可以直接使用TestUseCase类型:

class ViewModel {
    private let testUseCase: TestUseCase
    init(testUseCase: TestUseCase) {
        self.testUseCase = testUseCase
    }
}

这个方案逻辑最直观,适合不需要复用UseCase协议其他关联类型逻辑的场景。

内容的提问来源于stack exchange,提问作者sergeifabian

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 15:39:37