使用Swinject注册带关联类型的仓储协议触发泛型约束编译错误该如何解决
触发两次编译报错的核心原因和Swinject本身无关,是Swift类型系统的固有规则限制:只要协议带有
Self引用或者associatedtype(关联类型)声明,这类协议就只能作为泛型约束使用,无法作为独立的运行时类型直接参与实例传递、类型注册。不管是注册时传入SleepRepositoryProtocol.self作为注册key,还是业务代码里直接声明该协议类型的属性,都是违反了这条类型规则。
1. 拆分泛型协议结构,搭建可直接传递的上层抽象
原有的SleepRepositoryProtocol一般都会带关联类型声明,类似这样的结构:
// 带关联类型的泛型仓储协议 protocol SleepRepositoryProtocol { associatedtype Entity func fetchAll() -> [Entity] func save(_ entity: Entity) func delete(_ entity: Entity) }
要绕开关联类型的限制,首先要把所有仓储实现共有的、不依赖具体实体类型的方法抽出来,做成不带任何关联类型的上层抽象协议,再让原来的泛型仓储协议继承这个上层抽象:
// 无关联类型的上层抽象,可直接作为运行时类型使用 protocol AnySleepRepository { // 所有仓储实现通用的方法 func clearAllCache() func preloadLocalData() } // 原泛型仓储协议,继承上层抽象 protocol SleepRepositoryProtocol: AnySleepRepository { associatedtype Entity func fetchAll() -> [Entity] func save(_ entity: Entity) func delete(_ entity: Entity) }
2. 调整Swinject的注册逻辑
注册时不能再用带关联类型的SleepRepositoryProtocol作为注册key,要换成上层无关联类型的AnySleepRepository做注册标识,同时给不同持久化实现加命名区分,避免同key覆盖:
func assemble(container: Container) { // 注册CoreData实现 container.register(AnySleepRepository.self, name: "CoreData") { r in CoreDataSleepRepository( persistentContainer: r.resolve(NSPersistentContainer.self)! ) }.inObjectScope(.container) // 注册Realm实现 container.register(AnySleepRepository.self, name: "Realm") { r in RealmSleepRepository( realmConfig: r.resolve(Realm.Configuration.self)! ) }.inObjectScope(.container) }
这里的CoreData/Realm仓储实现只需要正常遵守泛型仓储协议、明确指定关联类型对应的实体类型即可,不需要额外适配:
final class CoreDataSleepRepository: SleepRepositoryProtocol { typealias Entity = SleepEntity let persistentContainer: NSPersistentContainer init(persistentContainer: NSPersistentContainer) { self.persistentContainer = persistentContainer } func fetchAll() -> [SleepEntity] { /* 具体实现 */ } func save(_ entity: SleepEntity) { /* 具体实现 */ } func delete(_ entity: SleepEntity) { /* 具体实现 */ } func clearAllCache() { /* 具体实现 */ } func preloadLocalData() { /* 具体实现 */ } }
3. 业务类的持有与解析适配
根据业务对仓储能力的使用需求,分两种场景调整代码,避免直接使用带关联类型的协议作为属性类型:
场景1:业务仅需要调用仓储的通用方法
直接持有上层无关联类型的协议即可,解析时也用这个类型从容器取实例:
class SleepBusinessViewModel { // 持有无关联类型的抽象协议,完全符合Swift类型规则 var repository: AnySleepRepository var items: [SleepEntity]? private let assembler: Assembler init(assembler: Assembler) { self.assembler = assembler // 按名称解析对应的持久化实现 repository = assembler.resolver.resolve( AnySleepRepository.self, name: "CoreData" )! } }
场景2:业务需要调用仓储的实体级操作(fetchAll/save/delete)
此时不能用非泛型类直接持有带关联类型的协议,需要把业务类改成泛型类,通过泛型约束声明仓储类型,由Swinject在注册泛型业务类时传入具体的仓储实现实例:
// 泛型业务类,通过泛型约束关联仓储实现类型 class SleepBusinessViewModel<Repository: SleepRepositoryProtocol> { // 泛型类型的仓储属性,符合类型规则 var repository: Repository var items: [Repository.Entity]? private let assembler: Assembler init(assembler: Assembler, repository: Repository) { self.assembler = assembler self.repository = repository } }
对应的注册逻辑要明确指定具体的仓储实现类型,不能传带关联类型的协议元类型:
container.register(SleepBusinessViewModel<CoreDataSleepRepository>.self) { r in SleepBusinessViewModel<CoreDataSleepRepository>( assembler: assembler, repository: r.resolve( AnySleepRepository.self, name: "CoreData" ) as! CoreDataSleepRepository ) }
- 不要为了快速消掉编译错误,强行给关联类型协议加
any前缀写成any SleepRepositoryProtocol。这种写法只是把编译器的静态检查暂时放宽,实际调用依赖关联类型的方法时,会因为类型擦除触发运行时崩溃,本质上没有解决类型不匹配的问题 - 多实现并行(CoreData/Realm)时,一定要给每个注册条目加明确的
name标识做区分,否则同个注册key下的后注册实现会直接覆盖前面的实例,导致运行时出现不符合预期的类型匹配结果 - 如果业务场景需要完全动态的类型解析(也就是不希望把业务类改成泛型类),可以参考苹果官方框架的通用处理方式,通过类型擦除做一层非泛型包装:把泛型仓储的所有关联类型操作都封装在包装类内部,对外暴露统一的无类型接口,既可以绕开Swift的关联类型协议限制,也不需要改动现有业务类的非泛型结构
内容的提问来源于stack exchange,提问作者itosun

