Как Swift ReferenceWritableKeyPath работает со свойством Optional? - PullRequest
7 голосов
/ 08 января 2020

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

@IBOutlet weak var iv: UIImageView!

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

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

Нет!

Значение необязательного типа 'UIImage?' должно быть развернуто до значения типа 'UIImage'

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


Что я на самом деле пытаюсь понять Так работает подписчик Combine Framework .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)

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

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

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

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

Теперь я могу обойти все это просто отлично, добавив map к моему конвейеру, который оборачивает UIImage в Необязательно, чтобы все типы соответствовали правильно. Но мой вопрос, как это действительно работает? Я имею в виду, почему я не должен делать это в первом коде, где я использую .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. Похоже, что он может pu sh опционально типа up конвейер в оператор .receive. Но я не понимаю, как это возможно.

1 Ответ

3 голосов
/ 11 января 2020

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

  • 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 обобщенных параметров типа c, таких как 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)

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

Поскольку Subscribers.Assign.init опубликован c, мы также можем использовать его напрямую, чтобы 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
    }
}

This не компилируется, с ошибкой value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'. Я не знаю, почему Свифт не может скомпилировать это. Он компилируется, если мы явно преобразуем 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
    }
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...