简体   繁体   中英

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. Code below which shows the exact setup I'm trying to achieve, written in a 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. 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 .

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.

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.

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.

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. So the easiest way would be to declare resource service class generic - class SomeResourceService<Repository: GenericRepository> .

But this solution has a big downside - you need to constraint generics everywhere this service would be involved.

You can drop generic constraint from the service declaration by declaring local as a concrete generic type. But how to transit from generic protocol to the concrete generic class?

There's a way. You can define a wrapper generic class which conforms to GenericRepository . It does not really implements its methods but rather passes to an object (which is real GenericRepository ) it wraps.

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 . You can adopt it in SomeResourceService without any alarm.

class SomeResourceService {
  let local: AnyGenericRepository<SomeResource>
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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