简体   繁体   English

Swift Generics - 尝试将专用子协议用作变量时,尝试将通用协议具体化失败

[英]Swift Generics - Attempting to make a generic protocol concrete fails when attempting to use specialised sub-protocol as variable

I want to know why my SomeResourceRepository is still generic, even though it is only defined in one case only, which is when I set ResourceType = SomeResource , which XCode formats as below with the where clause.我想知道为什么我的SomeResourceRepository仍然是通用的,即使它仅在一种情况下定义,即当我设置ResourceType = SomeResource时,XCode 使用 where 子句的格式如下。 Code below which shows the exact setup I'm trying to achieve, written in a Playground.下面的代码显示了我正在尝试实现的确切设置,写在 Playground 中。

I am trying to define a generic protocol for any given ResourceType such that the ResourceTypeRepository protocol then automatically requires the same set of functions, without having to copy-paste most of GenericRepository only to manually fill in the ResourceType for each Repository I make.我正在尝试为任何给定的ResourceType定义一个通用协议,以便ResourceTypeRepository协议自动需要相同的功能集,而不必复制粘贴大部分GenericRepository来手动填写我创建的每个存储库的 ResourceType。 The reason I need this as a protocol is because I want to be able to mock this for testing purposes later.我需要它作为协议的原因是因为我希望能够在以后模拟它以进行测试。 So I'll provide an implementation of said protocol somewhere else in the actual app.因此,我将在实际应用程序的其他地方提供所述协议的实现。

My interpretation of the code below is that it should work, because both SomeResourceLocalRepository and SomeResourceRemoteRepository are concrete, as I have eliminated the associated type by defining them "on top of" SomeResourceRepository , which is only defined where ResourceType == SomeResource .我对下面代码的解释是它应该可以工作,因为SomeResourceLocalRepositorySomeResourceRemoteRepository都是具体的,因为我通过“在” SomeResourceRepository之上定义它们来消除关联的类型,后者仅在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 }

Errors:错误:

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) {

I will probably have to find another way to accomplish this, but it is tedious and quite annoying as we will produce a lot of duplicate code, and when we decide to change the API of our repositories, we will have to manually change it for all the protocols as we don't follow a generic "parent" protocol in this work-around.我可能必须找到另一种方法来完成此操作,但这很乏味且很烦人,因为我们会产生大量重复代码,并且当我们决定更改存储库的 API 时,我们将不得不手动更改所有代码协议,因为我们在此解决方法中不遵循通用的“父”协议。

I have read How to pass protocol with associated type as parameter in Swift and the related question found in an answer to this question, as well as Specializing Generic Protocol and others.我已经阅读了如何在 Swift 中将具有关联类型的协议作为参数传递,以及在该问题的答案中找到的相关问题,以及专业通用协议等。

I feel like this should work, but it does not.我觉得这应该有效,但事实并非如此。 The end goal is a concrete protocol that can be used for dependency injection, eg container.register(.singleton) { ProtocolImplementation() as Protocol } as per Dip - A simple Dependency Injection Container , BUT without copy-pasting when the protocol's interface clearly can be made generic, like in the above.最终目标是一个可用于依赖注入的具体协议,例如根据 Dip 的container.register(.singleton) { ProtocolImplementation() as Protocol } - 一个简单的依赖注入容器,但是当协议的接口清晰时无需复制粘贴可以通用化,就像上面一样。

As swift provides a way to declare generic protocols (using associatedtype keyword) it's impossible to declare a generic protocol property without another generic constraint.由于 swift 提供了一种声明泛型协议的方法(使用associatedtype关键字),因此如果没有另一个泛型约束,就不可能声明泛型协议属性。 So the easiest way would be to declare resource service class generic - class SomeResourceService<Repository: GenericRepository> .所以最简单的方法是声明资源服务 class generic - class SomeResourceService<Repository: GenericRepository>

But this solution has a big downside - you need to constraint generics everywhere this service would be involved.但是这个解决方案有一个很大的缺点——你需要在涉及这个服务的任何地方限制 generics。

You can drop generic constraint from the service declaration by declaring local as a concrete generic type.您可以通过将local声明为具体的泛型类型来从服务声明中删除泛型约束。 But how to transit from generic protocol to the concrete generic class?但是如何从通用协议过渡到具体的通用 class?

There's a way.有办法。 You can define a wrapper generic class which conforms to GenericRepository .您可以定义一个符合GenericRepository的包装器通用 class 。 It does not really implements its methods but rather passes to an object (which is real GenericRepository ) it wraps.它并没有真正实现它的方法,而是传递给它包装的 object (这是真正的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
}

So now we have a concrete type which wraps real GenericRepository .所以现在我们有一个具体的类型,它包装了真正的GenericRepository You can adopt it in SomeResourceService without any alarm.您可以在SomeResourceService中采用它而无需任何警报。

class SomeResourceService {
  let local: AnyGenericRepository<SomeResource>
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM