Создание свойства строгого универсального типа «self»

#swift #generics

#swift #дженерики

Вопрос:

Я хочу создать свойство для класса, который использует тип класса в качестве универсального параметра, и мне трудно его решить.

 open class ResponseProcessor {

  required public init() {

  }

  var success: ((_ responseProcessor: ResponseProcessor) -> Void)?

  func process() {

    success?(self)

  }

}

class TestProcessor: ResponseProcessor {

  var result: String?

  override func process() {

    result = "Some Result"

    super.process()

  }

}

open class Request<ResponseProcessorType: ResponseProcessor> {

  var success: ((_ responseProcessor: ResponseProcessor) -> Void)?

  func doRequest() {

    let responseProcessor = ResponseProcessorType.init()
    responseProcessor.success = success
    responseProcessor.process()

  }

}

class TestRequest: Request<TestProcessor> {

}

let testRequest = TestRequest()
testRequest.success = { (responseProcessor) in
  // This line reports an error, but I want it to know what 
  // type the responseProcessor is.
  print(responseProcessor.result) 

}
testRequest.doRequest()
  

Я хочу иметь возможность назначать подзапрос .request переменной, но я не могу из-за строгой универсальной типизации.

Итак, я хотел бы иметь возможность сказать: « request свойство a ResponseProcessor должно иметь тип Request<WhateverThisClassIs> , но я не могу понять, как это выразить или объявить его так, чтобы это работало.

Должно получиться, что testProcessor.request это тип HTTPRequest<TestProcessor> , но, очевидно, этого не происходит.

Комментарии:

1. Как вы хотите, чтобы ваш вызывающий код выглядел, если бы это работало так, как вам хотелось бы? Какой алгоритм будет читать testProcessor.request и что он будет делать со значением? Почему циклическая зависимость между ResponseProcessor и Request? Это очень неудобно. Какую общую валюту (URLResponse, URLRequest, Data) вы могли бы использовать, чтобы процессор работал? Начиная с варианта использования (вызывающего кода), будет зависеть от того, как должны выглядеть ваши типы. У Swift нет способа выразить общую ковариацию, но неясно, чего вы действительно хотите здесь в любом случае.

2. @RobNapier Я сделал вариант использования более реалистичным — я пытался упростить его для удобства чтения.

3. Вы упоминаете request переменную, но в вашем коде ее нет. Не могли бы вы уточнить это?

4. Когда вы говорите: «Я хочу, чтобы он знал, какой тип у responseProcessor», это то, что вы на самом деле имеете в виду? Вы действительно хотите знать тип responseProcessor? (Потому что это тривиально: type(of: responseProcessor) .) Или вы действительно имеете в виду, что хотите что -то result сделать? С чем вы хотите что-то сделать result ? Как будет выглядеть следующая строка кода, которой нет print ?

5.В вашем обновленном коде проблема вообще не в дженериках. Это то, что success передает a ResponseProcessor и ResponseProcessor не имеет result свойства. Итак, как бы это выглядело, если бы это было просто TestRequest , а не классы, дженерики или что-то еще. Что бы это сделало, и что бы что-то, чего нет TestRequest . Как они дублируют код? И оттуда мы можем извлечь абстракцию. Сначала напишите конкретные версии.

Ответ №1:

Я не уверен, ответит ли это на ваш вопрос или нет, но, возможно, это поможет вам лучше. На ваш заданный вопрос ответ заключается в том, что в Swift нет общей ковариации. То, что вы пытаетесь написать, невозможно. Общая ковариация на самом деле не исправит ваш код, потому что здесь у вас много других проблем с типами (ваша последняя версия, вероятно, нарушает принцип подстановки Лискова, что означает, что она нарушает смысл наследования классов). Но я не думаю, что вы на самом деле хотите то, что пытаетесь написать вообще.

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

Во-первых, сам низкоуровневый сетевой стек должен использовать запросы URL и возвращать данные. Вот и все. Он не должен пытаться работать с типами моделей. Именно здесь люди всегда сходят с рельсов. Таким образом, запрос представляет собой URL-запрос и обработчик завершения:

 struct Request {
    let urlRequest: URLRequest
    let completion: (Result<Data, Error>) -> Void
}
  

И клиент их использует.

 final class NetworkClient {
    func fetch(_ request: Request) {
        URLSession.shared.dataTask(with: request.urlRequest) { (data, _, error) in
            if let error = error { request.completion(.failure(error)) }
            else if let data = data { request.completion(.success(data)) }
            }.resume()
    }
}
  

Теперь мы обычно не хотим разговаривать с URLSession при тестировании. Вероятно, мы хотим вернуть предварительно сохраненные данные. Итак, мы создаем одно из них.

 final class TestClient {
    enum ClientError: Error {
        case underflow
    }
    var responses: [Result<Data, Error>]
    init(responses: [Result<Data, Error>]) { self.responses = responses }
    func fetch(_ request: Request) {
        if let response = responses.first {
            responses.removeFirst()
            request.completion(response)
        } else {
            request.completion(.failure(ClientError.underflow))
        }
    }
}
  

Я отмечаю вещи final class , потому что это разумно ссылочные типы, но я хочу прояснить, что я нигде здесь не использую наследование классов. (Не стесняйтесь оставлять «final» в своем собственном коде; это немного педантично и обычно не требуется.)

Чем эти две вещи похожи? Они совместно используют протокол:

 protocol Client {
    func fetch(_ request: Request)
}
  

Отлично. Теперь я могу делать такие вещи, как:

 let client: Client = TestClient(responses: [])
  

Отсутствие связанных типов означает, что Client это совершенно нормально как тип.

Но получение данных обратно довольно уродливо. Нам нужен тип, например User.

 struct User: Codable, Equatable {
    let id: Int
    let name: String
}
  

Как нам это сделать? Нам просто нужен способ создания запроса, который извлекает декодируемый:

 extension Request {
    init<Model: Decodable>(fetching: Model.Type,
                           from url: URL,
                           completion: @escaping (Result<Model, Error>) -> Void) {
        self.urlRequest = URLRequest(url: url)
        self.completion = { data in
            completion(Result {
                try JSONDecoder().decode(Model.self, from: data.get())})
        }
    }
}
  

Обратите внимание, что запрос по-прежнему ничего не знает о моделях? И клиент ничего не знает о моделях. Есть только этот инициализатор запроса, который принимает тип модели и оборачивает его таким образом, чтобы он мог принимать данные и возвращать модель.

Вы можете использовать этот подход намного дальше. Вы можете написать клиент, который обертывает клиент и изменяет запрос, добавляя заголовки, например.

 struct AddHeaders: Client {
    let base: Client
    let headers: [String: String]

    func fetch(_ request: Request) {
        var urlRequest = request.urlRequest
        for (key, value) in headers {
            urlRequest.addValue(value, forHTTPHeaderField: key)
        }

        base.fetch(Request(urlRequest: urlRequest,
                           completion: request.completion))
    }
}

let client = AddHeaders(base: NetworkClient(),
                        headers: ["Authorization": "Token ...."])
  

Здесь нет подклассов, нет общих типов, только один протокол (который не имеет связанных типов) и один общий метод. Но вы можете подключить самые разные серверные части и скомпоновать любую операцию, которая может быть выполнена в соответствии с одним из нескольких преобразований (Запрос -> Запрос, запрос -> Данные, Данные -> Void).

Я надеюсь, что это соответствует некоторым из того, что вы получаете в своем вопросе. Желаю удачи.