首页 > 解决方案 > Swift Generics - 尝试使用专用子协议作为变量时,尝试使通用协议具体化失败

问题描述

我想知道为什么 mySomeResourceRepository仍然是通用的,即使它仅在一种情况下定义,即当我设置ResourceType = SomeResource时,XCode 使用 where 子句格式化如下。下面的代码显示了我正在尝试实现的确切设置,写在 Playground 中。

我正在尝试为任何给定的协议定义一个通用协议,ResourceType以便该ResourceTypeRepository协议自动需要相同的功能集,而不必复制粘贴大部分内容GenericRepository来手动填写我创建的每个存储库的 ResourceType。我需要它作为协议的原因是因为我希望能够在以后模拟它以进行测试。因此,我将在实际应用程序的其他地方提供所述协议的实现。

我对下面代码的解释是它应该可以工作,因为SomeResourceLocalRepositorySomeResourceRemoteRepository都是具体的,因为我已经通过在“顶部”定义它们来消除关联类型SomeResourceRepository,这仅在 where 定义ResourceType == SomeResource

import Foundation

struct SomeResource: Identifiable {
    let id: String
    let name: String
}

struct WhateverResource: Identifiable {
    let id: UUID
    let count: UInt
}

protocol GenericRepository: class where ResourceType: Identifiable {
    associatedtype ResourceType

    func index() -> Array<ResourceType>
    func show(id: ResourceType.ID) -> ResourceType?
    func update(resource: ResourceType)
    func delete(id: ResourceType.ID)
}

protocol SomeResourceRepository: GenericRepository where ResourceType == SomeResource {}
protocol SomeResourceLocalRepository: SomeResourceRepository {}
protocol SomeResourceRemoteRepository: SomeResourceRepository {}

class SomeResourceLocalRepositoryImplementation: SomeResourceLocalRepository {
    func index() -> Array<SomeResource> {
        return []
    }

    func show(id: String) -> SomeResource? {
        return nil
    }

    func update(resource: SomeResource) {
    }

    func delete(id: String) {
    }
}

class SomeResourceService {
    let local: SomeResourceLocalRepository

    init(local: SomeResourceLocalRepository) {
        self.local = local
    }
}

// Some Dip code somewhere
// container.register(.singleton) { SomeResourceLocalRepositoryImplementation() as SomeResourceLocalRepository }

错误:

error: Generic Protocols.xcplaygroundpage:45:16: error: protocol 'SomeResourceLocalRepository' can only be used as a generic constraint because it has Self or associated type requirements
let local: SomeResourceLocalRepository
           ^

error: Generic Protocols.xcplaygroundpage:47:17: error: protocol 'SomeResourceLocalRepository' can only be used as a generic constraint because it has Self or associated type requirements
    init(local: SomeResourceLocalRepository) {

我可能不得不找到另一种方法来完成此操作,但这很乏味且很烦人,因为我们会产生大量重复的代码,并且当我们决定更改存储库的 API 时,我们将不得不手动更改所有代码协议,因为我们在此解决方法中不遵循通用的“父”协议。

我已阅读如何在 Swift 中将具有关联类型的协议作为参数传递,以及在该问题的答案中找到的相关问题,以及专业化通用协议等。

我觉得这应该有效,但事实并非如此。最终目标是一个可用于依赖注入的具体协议,例如container.register(.singleton) { ProtocolImplementation() as Protocol }按照Dip - 一个简单的依赖注入容器,但是当协议的接口显然可以通用时,无需复制粘贴,就像上面一样。

标签: swiftgenericsdependency-injectionswift-protocols

解决方案


由于 swift 提供了一种声明泛型协议的方法(使用associatedtype关键字),因此不可能在没有另一个泛型约束的情况下声明泛型协议属性。所以最简单的方法是声明资源服务类泛型 - class SomeResourceService<Repository: GenericRepository>

但是这个解决方案有一个很大的缺点——你需要在任何涉及这个服务的地方限制泛型。

您可以通过声明local为具体的泛型类型来从服务声明中删除泛型约束。但是如何从泛型协议过渡到具体的泛型类呢?

有办法的。您可以定义一个符合GenericRepository. 它并没有真正实现它的方法,而是传递给GenericRepository它包装的一个对象(它是真实的)。

class AnyGenericRepository<ResourceType: Identifiable>: GenericRepository {
  // any usage of GenericRepository must be a generic argument
  init<Base: GenericRepository>(_ base: Base) where Base.ResourceType == ResourceType {
    // we cannot store Base as a class property without putting it in generics list
    // but we can store closures instead
    indexGetter = { base.index() }
    // and same for other methods or properties
    // if GenericRepository contained a generic method it would be impossible to make
  }

  private let indexGetter: () -> [ResourceType] {
    indexGetter()
  }

  // ... other GenericRepository methods
}

所以现在我们有了一个包含 real 的具体类型GenericRepositorySomeResourceService您可以在没有任何警报的情况下采用它。

class SomeResourceService {
  let local: AnyGenericRepository<SomeResource>
}

推荐阅读