Swift ReferenceWritableKeyPath 如何与 Optional 属性 一起使用?

How does Swift ReferenceWritableKeyPath work with an Optional property?

Ground of Being:在阅读之前了解您不能将 UIImage 分配给图像视图出口的 image 属性 会有所帮助键路径 \UIImageView.image。这是 属性:

@IBOutlet weak var iv: UIImageView!

现在,这会编译吗?

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

没有!

Value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'

好的,现在我们准备好实际用例了。


我实际上想了解的是 Combine 框架 .assign 订户如何在幕后工作。为了进行实验,我尝试使用自己的 Assign 对象。在我的示例中,我的发布者管道生成一个 UIImage 对象,我将它分配给 UIImageView 属性 self.iv.

image 属性

如果我们使用 .assign 方法,这将编译并工作:

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

所以,我对自己说,为了看看它是如何工作的,我将删除 .assign 并将其替换为我自己的 Assign 对象:

let pub = URLSession.shared.dataTaskPublisher(for: url)
    .map {[=15=].data}
    .replaceError(with: Data())
    .compactMap { UIImage(data:[=15=]) }
    .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 键路径?关键路径如何与可选属性一起使用似乎有一些技巧,但我不知道它是什么。


经过 Martin R 的一些输入后,我意识到如果我们明确键入 pub 来生成 UIImage?,我们得到的效果与添加 map 相同,该 map 将 UIImage 包装在 Optional 中。所以这个编译和工作

let pub : AnyPublisher<UIImage?,Never> = URLSession.shared.dataTaskPublisher(for: url)
    .map {[=17=].data}
    .replaceError(with: Data())
    .compactMap { UIImage(data:[=17=]) }
    .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 的工作原理。它似乎能够 push 类型 up 管道的可选性进入 .receive 运算符。但我不明白这怎么可能。

您(马特)可能至少已经知道其中的一些内容,但这里有一些供其他读者参考的事实:

  • Swift 一次推断一个完整语句的类型,但不跨语句推断类型。

  • Swift 允许类型推断自动将类型 T 的对象提升为类型 Optional<T>,如果需要进行语句类型检查。

  • Swift 还允许类型推断自动将类型 (A) -> B 的闭包提升为类型 (A) -> B?。换句话说,这编译:

    let a: (Data) -> UIImage? = { UIImage(data: [=10=]) }
    let b: (Data) -> UIImage?? = a
    

    这让我感到惊讶。我在调查你的问题时发现了它。

现在让我们考虑使用assign:

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

Swift 同时对整个语句进行类型检查。由于 \UIImageView.imageValue 类型是 UIImage?,而 self.iv 的类型是 UIImageView!,Swift 必须做两件“自动”事情对这条语句进行类型检查:

  • 它必须将闭包{ UIImage(data: [=30=]) }从类型(Data) -> UIImage?提升到类型(Data) -> UIImage??,以便compactMap可以剥离一层Optional 并使 Output 类型为 UIImage?.

  • 它必须隐式展开 iv,因为 Optional<UIImage> 没有名为 image 的 属性,但是 UIImage 有。

这两个操作让 Swift 成功地对语句进行类型检查。

现在假设我们把它分成三个语句:

let p1 = Just(Data())
    .compactMap { UIImage(data: [=12=]) }
    .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 语句进行类型检查。 p1Output类型是UIImagea1Input类型是UIImage?。它们是不同的,因此 Swift 无法成功地对语句进行类型检查。 Swift 不支持 Optional 提升泛型类型参数,如 InputOutput。所以这不会编译。

我们可以通过强制 p1Output 类型为 UIImage?:

来进行类型检查
let p1: AnyPublisher<UIImage?, Never> = Just(Data())
    .compactMap { UIImage(data: [=13=]) }
    .receive(on: DispatchQueue.main)
    .eraseToAnyPublisher()
let a1 = Subscribers.Assign(object: self.iv, keyPath: \.image)
p1.subscribe(a1)

这里,我们强制Swift提升闭包类型。我使用 eraseToAnyPublisher 是因为 p1 的类型很难拼写出来。

由于Subscribers.Assign.init是public,我们也可以直接用它来使Swift推断出所有的类型:

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

Swift 类型检查成功。它与前面使用 .assign 的语句基本相同。请注意,它为 p2 推断类型 () 因为这就是 .subscribe returns 这里的内容。


现在,回到基于键路径的分配:

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
    }
}

我认为这可能是编译器中的错误。