Как я могу использовать Type Erasure с протоколом, использующим связанный тип

Я работаю над проектом, в котором есть сетевой клиент, который в основном следует приведенному ниже шаблону.

protocol EndpointType {
    var baseURL: String { get }
}

enum ProfilesAPI {
    case fetchProfileForUser(id: String)
}

extension ProfilesAPI: EndpointType {
    var baseURL: String {
        return "https://foo.bar"
    }
}

protocol ClientType: class {
    associatedtype T: EndpointType
    func request(_ request: T) -> Void
}

class Client<T: EndpointType>: ClientType {
    func request(_ request: T) -> Void {
        print(request.baseURL)
    }
}

let client = Client<ProfilesAPI>()

client.request(.fetchProfileForUser(id: "123"))

В рамках приведения в порядок этого проекта и написания тестов я обнаружил, что невозможно внедрить client при соответствии протоколу ClientType.

let client: ClientType = Client<ProfilesAPI>() выдает ошибку:

ошибка: элемент «запрос» не может использоваться для значения типа протокола «ClientType»; вместо этого используйте общее ограничение

Я хотел бы сохранить текущий шаблон ... = Client<ProfilesAPI>()

Можно ли добиться этого с помощью стирания типа? Я читал, но не уверен, как это сделать.


person Tim J    schedule 11.03.2019    source источник
comment
Чего вы пытаетесь достичь? Зачем нужно указывать тип client   -  person Robert Dresler    schedule 11.03.2019
comment
Существует несколько конечных точек, исходный код был написан для использования дженериков, например Client<RolesAPI>() и так далее.   -  person Tim J    schedule 11.03.2019
comment
Но какова ваша конкретная потребность? Точное использование   -  person Robert Dresler    schedule 11.03.2019
comment
Я хотел бы иметь возможность настроить вызовы API, используя простой синтаксис, такой как client.request(.fetchProfileForUser(id: "123")) со значением перечисления в пределах request(...), установленным с использованием случая из перечисления, которое соответствует EndpointType. Это перечисление устанавливается при объявлении экземпляра client.   -  person Tim J    schedule 11.03.2019
comment
Это очень похоже на то, как работает Moya, однако я не писал это приложение, я только унаследовал кодовую базу, поэтому изначально я не на 100% понятен в рассуждениях.   -  person Tim J    schedule 11.03.2019


Ответы (1)


На ваш актуальный вопрос тип ластика прост:

final class AnyClient<T: EndpointType>: ClientType {
    let _request: (T) -> Void
    func request(_ request: T) { _request(request) }

    init<Client: ClientType>(_ client: Client) where Client.T == T {
        _request = client.request
    }
}

Вам понадобится одна из этих _func/func пар для каждого требования в протоколе. Вы можете использовать его следующим образом:

let client = AnyClient(Client<ProfilesAPI>())

И затем вы можете создать тестовую обвязку, например:

class RecordingClient<T: EndpointType>: ClientType {
    var requests: [T] = []
    func request(_ request: T) -> Void {
        requests.append(request)
        print("recording: \(request.baseURL)")
    }
}

И используйте это вместо этого:

let client = AnyClient(RecordingClient<ProfilesAPI>())

Но я действительно не рекомендую этот подход, если вы можете избежать его. Ластики для шрифтов — головная боль. Вместо этого я бы заглянул внутрь Client и извлек неуниверсальную часть в протокол ClientEngine, который не требует T. Затем сделайте это заменяемым при создании файла Client. Тогда вам не нужны стиратели типов, и вам не нужно предоставлять вызывающим сторонам дополнительный протокол (только EndpointType).

Например, часть двигателя:

protocol ClientEngine: class {
    func request(_ request: String) -> Void
}

class StandardClientEngine: ClientEngine {
    func request(_ request: String) -> Void {
        print(request)
    }
}

Клиент, который держит двигатель. Обратите внимание, как он использует параметр по умолчанию, так что вызывающим не нужно ничего менять.

class Client<T: EndpointType> {
    let engine: ClientEngine
    init(engine: ClientEngine = StandardClientEngine()) { self.engine = engine }

    func request(_ request: T) -> Void {
        engine.request(request.baseURL)
    }
}

let client = Client<ProfilesAPI>()

И снова вариант записи:

class RecordingClientEngine: ClientEngine {
    var requests: [String] = []
    func request(_ request: String) -> Void {
        requests.append(request)
        print("recording: \(request)")
    }
}

let client = Client<ProfilesAPI>(engine: RecordingClientEngine())
person Rob Napier    schedule 27.03.2019