Почему вывод типа охранника ломает?

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

Вот примерный код для настройки:

import Foundation

struct Model: Decodable {
    var i: String
}

let jsonString = """
{
    "i": "yes"
}
"""

let jsonData = jsonString.data(using: .utf8)
let theModel = try JSONDecoder().decode(Model.self, from: jsonData!)

struct Response<T> {
    var decodedData: T?
}

enum Result<Value> {
    case success(Value)
    case failure
}

struct Client {
    static let shared = Client()
    private init() {}

    func execute<T: Decodable>(completion: (Response<T>) -> ()) {
        let decodedData = try! JSONDecoder().decode(T.self, from: jsonData!)
        completion(Response(decodedData: decodedData))
    }
}

Вот проблема:

struct ServicesA {
    func loadSomething(completion: (Result<Model>) -> ()) {
        Client.shared.execute { result in      // error: generic parameter 'T' could not be inferred
            guard let decodedData = result.decodedData else { return }
            completion(Result.success(decodedData))
        }
    }
}

struct ServicesB {
    func loadSomething(completion: (Result<Model>) -> ()) {
        Client.shared.execute { result in
            completion(Result.success(result.decodedData!))
        }
    }
}

ServicesA ломается тогда как ServicesB компилирует.

Как вы можете видеть, единственная разница guard let decodedData = result.decodedData else { return }, Это нарушает вывод типа так, что Client.shared.execute функция жалуется, что T не может быть выведено

Интересно, почему это произошло, и как лучше всего справиться с этой проблемой.

4 ответа

Решение

TLDR; компиляция однострочных закрытий отличается от многострочных

Длинный ответ: Давайте на некоторое время забудем однолинейные закрытия. Когда мы пишем универсальную функцию, принимающую универсальный тип аргумента и затем вызываем эту функцию, компилятору необходимо знать во время вызова, какой тип функции требуется, то есть тип ее аргумента. Теперь рассмотрим этот аргумент как закрытие. Опять же, компилятор должен знать, что это за тип замыкания (сигнатура функции) во время вызова. Эта информация должна быть доступна в сигнатуре замыкания (т. Е. Аргумент и тип возвращаемого значения), компилятор не беспокоится (и правильно делает) о теле замыкания. Таким образом, служба A ведет себя отлично, как и ожидалось от компилятора, т. Е. Подпись замыкания не дает никакой подсказки о ее типе.

Теперь приходит в одной строке закрытия. Компилятор Swift имеет встроенную оптимизацию вывода типов только для однострочных замыканий. Когда вы пишете однострочное закрытие, компилятор сначала запускает его вывод типа и пытается выяснить о замыкании, включая его тип возврата и т. Д., Из его единственной строки тела. Этот тип вывода вступает в силу для однострочных замыканий (с контекстом обобщения или без него). Этот тип вывода является причиной того, что ваш сервис B работает

Поэтому я перефразирую вопрос как "Почему вывод типа работает для замыканий на одну строку?", Потому что это дополнительная функция, предоставляемая Swift только для замыканий на одну строку. Как только вы перейдете к закрытию нескольких строк (это не относится к конкретному оператору защиты, то же самое произойдет, если вы также положите print ("hello world")), этот тип вывода больше не доступен. Хотя для многострочных замыканий могут быть доступны другие проверки вывода типов.

Что вы можете сделать, это просто предоставить информацию о типе в подписи замыкания, т. Е. (Результат: Response)

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

let x = {
    return 1
}()

                // x == Int(1)

let y = {       // error: unable to infer complex closure return type; add explicit type to disambiguate
    print("hi")
    return 2
}()

https://forums.swift.org/t/problems-with-closure-type-inference/11859/2

Вы должны напечатать его result с Result, так что компилятор может знать о T

struct ServicesA {
    func loadSomething(completion:(Result<Model>) -> ()) {
        Client.shared.execute { (result:Response<Model>) in
            guard let data = result.decodedData else {return}
            completion(Result.success(data))
        }
    }
}

Проблема в том, что вы ссылаетесь data переменная, но компилятор не знает, что это за тип (потому что это Generic). Попробуй это:

struct ServicesA {
    func loadSomething<T:Model>(completion:(Result<T>) -> ()) {
        Client.shared.execute { result:Response<T> in
            guard let decodedData = result.decodedData else { return }
            completion(Result.success(decodedData))
        }
    }
}
Другие вопросы по тегам