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
相关产品推荐
相关产品推荐

