#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
? Как будет выглядеть следующая строка кода, которой нет5.В вашем обновленном коде проблема вообще не в дженериках. Это то, что
success
передает aResponseProcessor
и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).
Я надеюсь, что это соответствует некоторым из того, что вы получаете в своем вопросе. Желаю удачи.