Как Swift ReferenceWritableKeyPath работает с необязательным свойством?

Основание существования: перед чтением будет полезно знать, что вы не можете назначить UIImage свойству image выхода просмотра изображений с помощью ключевого пути \UIImageView.image. Вот свойство:

@IBOutlet weak var iv: UIImageView!

Теперь это будет компилироваться?

    let im = UIImage()
    let kp = \UIImageView.image
    self.iv[keyPath:kp] = im // error

No!

Значение необязательного типа «UIImage?» необходимо развернуть до значения типа 'UIImage'

Хорошо, теперь мы готовы к фактическому использованию.


На самом деле я пытаюсь понять, как подписчик платформы Combine .assign работает за кулисами. Чтобы поэкспериментировать, я попробовал использовать свой собственный объект Assign. В моем примере конвейер издателя создает объект UIImage, и я назначаю его свойству image свойства UIImageView self.iv.

Если мы используем метод .assign, он компилируется и работает:

URLSession.shared.dataTaskPublisher(for: url)
    .map {$0.data}
    .replaceError(with: Data())
    .compactMap { UIImage(data:$0) }
    .receive(on: DispatchQueue.main)
    .assign(to: \.image, on: self.iv)
    .store(in:&self.storage)

Итак, говорю я себе, чтобы увидеть, как это работает, я удалю .assign и заменю его своим собственным объектом Assign:

let pub = URLSession.shared.dataTaskPublisher(for: url)
    .map {$0.data}
    .replaceError(with: Data())
    .compactMap { UIImage(data:$0) }
    .receive(on: DispatchQueue.main)

let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image)
pub.subscribe(assign) // error
// (and we will then wrap in AnyCancellable and store)

Блап! Мы не можем этого сделать, потому что UIImageView.image - это необязательный UIImage, а мой издатель создает простой и простой UIImage.

Я попытался обойти это, развернув Optional в ключевом пути:

let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image!)
pub.subscribe(assign)

Круто, что компилируется. Но он вылетает во время выполнения, предположительно из-за того, что изображение представления изображения изначально nil.

Теперь я могу нормально обойти все это, добавив map к моему конвейеру, который объединяет UIImage в Optional, чтобы все типы соответствовали правильно. Но у меня вопрос: как это на самом деле работает? Я имею в виду, почему бы мне не сделать это в первом коде, где я использую .assign? Почему я могу указать здесь путь к клавишам .image? Кажется, есть некоторая хитрость в том, как ключевые пути работают с дополнительными свойствами, но я не знаю, что это такое.


После некоторого ввода от Мартина Р. я понял, что если мы введем pub явно как производящий UIImage?, мы получим тот же эффект, что и добавление map, которое обертывает UIImage в Optional. Итак, это компилируется и работает

let pub : AnyPublisher<UIImage?,Never> = URLSession.shared.dataTaskPublisher(for: url)
    .map {$0.data}
    .replaceError(with: Data())
    .compactMap { UIImage(data:$0) }
    .receive(on: DispatchQueue.main)
    .eraseToAnyPublisher()

let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image)
pub.subscribe(assign)
let any = AnyCancellable(assign)
any.store(in:&self.storage)

Это все еще не объясняет, как работает оригинальный .assign. Похоже, что он может протолкнуть опциональность типа вверх по конвейеру в оператор .receive. Но я не понимаю, как это возможно.


person matt    schedule 07.01.2020    source источник
comment
Просто любопытно: что происходит в вашем первом коде, если self.iv.image равен нулю?   -  person Martin R    schedule 08.01.2020
comment
@MartinR Работает нормально. (В реальной жизни я перехожу к .store AnyCancellable, как и следовало ожидать, и код загружается и показывает изображение очень хорошо.) Вот почему я сбит с толку; Я не обнаруживал проблемы, пока не попытался разобраться в том, что .assign делает за кулисами, предоставив моего собственного подписчика Assign, подписавшись на него и продвигая его на AnyCancellable (не показано).   -  person matt    schedule 08.01.2020
comment
@MartinR Я добавлю это к q.   -  person matt    schedule 08.01.2020
comment
У меня пока нет опыта работы с Combine, поэтому я ничего «не ожидаю» :) - Но я заметил, что предполагаемый тип let pub = ... - это Publishers.ReceiveOn<Publishers.CompactMap<Publishers.ReplaceError<Publishers.Map<URLSession.DataTaskPublisher, Data>>, UIImage>, DispatchQueue>. Если вы аннотируете его явно, но с заменой UIImage на UIImage?, ошибка исчезнет. Похоже, что это то, что компилятор делает в первом примере из .assign(to: \.image, on: self.iv), поскольку self.iv.image является необязательным.   -  person Martin R    schedule 08.01.2020
comment
Ошибка также исчезнет, ​​если вы замените compactMap на map - имеет ли это смысл? Я не уверен.   -  person Martin R    schedule 08.01.2020
comment
@MartinR Если вы разорвите цепочку на receiveOn, ее результат будет набран как UIImage. Если вы добавите assign, receiveOn вывод будет набран как UIImage ?. Это как если бы assign мог подтолкнуть опциональность вверх по конвейеру. Я не понимаю, как это происходит.   -  person matt    schedule 08.01.2020
comment
Вы также правы в том, что если мы eraseToAnyPublisher и явно введем pub generic как параметризующий UIImage? вся проблема уходит. Я добавлю это к вопросу.   -  person matt    schedule 08.01.2020


Ответы (1)


Вы (Мэтт), вероятно, уже знаете хотя бы часть этого, но вот некоторые факты для других читателей:

  • Swift определяет типы для одного целого оператора за раз, но не для нескольких операторов.

  • Swift позволяет при выводе типа автоматически преобразовывать объект типа T в тип Optional<T>, если это необходимо для проверки типа оператора.

  • Swift также позволяет вывод типа автоматически продвигать замыкание типа (A) -> B на тип (A) -> B?. Другими словами, это компилирует:

    let a: (Data) -> UIImage? = { UIImage(data: $0) }
    let b: (Data) -> UIImage?? = a
    

    Для меня это стало неожиданностью. Я обнаружил это, исследуя вашу проблему.

Теперь давайте рассмотрим использование assign:

let p0 = Just(Data())
    .compactMap { UIImage(data: $0) }
    .receive(on: DispatchQueue.main)
    .assign(to: \.image, on: self.iv)

Swift проверяет тип этого оператора одновременно. Поскольку тип \UIImageView.image Value - UIImage?, а тип self.iv - UIImageView!, Swift должен сделать две «автоматические» вещи, чтобы выполнить проверку типа этого оператора:

  • Он должен продвигать закрытие { UIImage(data: $0) } от типа (Data) -> UIImage? к типу (Data) -> UIImage??, чтобы compactMap мог отделить один уровень Optional и сделать тип Output UIImage?.

  • Он должен неявно разворачивать iv, потому что Optional<UIImage> не имеет свойства с именем image, но UIImage имеет.

Эти два действия позволяют Swift успешно проверять тип оператора.

Теперь предположим, что мы разбили его на три утверждения:

let p1 = Just(Data())
    .compactMap { UIImage(data: $0) }
    .receive(on: DispatchQueue.main)
let a1 = Subscribers.Assign(object: self.iv, keyPath: \.image)
p1.subscribe(a1)

Swift сначала проверяет тип оператора let p1. Ему не нужно продвигать тип закрытия, поэтому он может вывести Output тип UIImage.

Затем Swift проверяет тип оператора let a1. Он должен неявно разворачиваться iv, но Optional продвижение не требуется. Он выводит тип Input как UIImage?, потому что это тип Value ключевого пути.

Наконец, Swift пытается проверить тип оператора subscribe. Тип Output для p1 - UIImage, а тип Input для a1 - UIImage?. Они разные, поэтому Swift не может успешно проверить тип оператора. Swift не поддерживает Optional продвижение параметров универсального типа, таких как Input и Output. Так что это не компилируется.

Мы можем выполнить эту проверку типа, установив Output тип p1 равным UIImage?:

let p1: AnyPublisher<UIImage?, Never> = Just(Data())
    .compactMap { UIImage(data: $0) }
    .receive(on: DispatchQueue.main)
    .eraseToAnyPublisher()
let a1 = Subscribers.Assign(object: self.iv, keyPath: \.image)
p1.subscribe(a1)

Здесь мы заставляем Swift продвигать тип закрытия. Я использовал eraseToAnyPublisher, потому что в противном случае тип p1 слишком уродлив, чтобы описывать его по буквам.

Поскольку Subscribers.Assign.init является общедоступным, мы также можем использовать его напрямую, чтобы Swift определял все типы:

let p2 = Just(Data())
    .compactMap { UIImage(data: $0) }
    .receive(on: DispatchQueue.main)
    .subscribe(Subscribers.Assign(object: self.iv, keyPath: \.image))

Swift успешно проверяет это. По сути, это то же самое, что использовалось ранее .assign. Обратите внимание, что он подразумевает тип () для p2, потому что это то, что здесь возвращает .subscribe.


Теперь вернемся к назначению на основе пути:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv[keyPath: kp] = im
    }
}

Это не компилируется с ошибкой value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'. Я не знаю, почему Swift не может это скомпилировать. Он компилируется, если мы явно конвертируем im в UIImage?:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv[keyPath: kp] = .some(im)
    }
}

Он также компилируется, если мы изменим тип iv на UIImageView? и опционально сделаем присвоение:

class Thing {
    var iv: UIImageView? = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv?[keyPath: kp] = im
    }
}

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

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv![keyPath: kp] = im
    }
}

И он не компилируется, если мы просто опционально определили назначение:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv?[keyPath: kp] = im
    }
}

Думаю, это может быть ошибка компилятора.

person rob mayoff    schedule 11.01.2020
comment
Я думаю, что ключевое утверждение здесь может заключаться в том, что Swift не поддерживает необязательное продвижение параметров универсального типа, таких как ввод и вывод. Это именно то, что мне казалось, было, и я не мог понять, почему это происходит когда .assign подключал что-то, но не когда я что-то подключал. Я думаю, что вы продвигаете закрытие - это то объяснение, которое я ищу. - person matt; 11.01.2020
comment
Последние четыре параллельны (но не совсем такие же?) Тому, что я разработал в родственном вопросе этого вопроса: stackoverflow.com/questions/59653363/ У меня есть фактически прикрепил его к существующему отчету об ошибке, bugs.swift.org/browse/SR-11184 < / а>. - person matt; 11.01.2020
comment
Да, похоже на ту же ошибку. - person rob mayoff; 11.01.2020
comment
То же продвижение действует и для массива. Если мое закрытие имеет тип () -> Array<Int> и я назначаю его там, где ожидается функция типа () -> Array<Optional<Int>>, это допустимо, и закрытие при выполнении теперь фактически создает массив необязательных Ints. - person matt; 20.01.2020