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

Posted

技术标签:

【中文标题】Swift ReferenceWritableKeyPath 如何与 Optional 属性一起使用?【英文标题】:How does Swift ReferenceWritableKeyPath work with an Optional property? 【发布时间】:2020-04-25 11:20:18 【问题描述】:

存在的基础:在阅读之前,了解您不能通过 keypath \UIImageView.image 将 UIImage 分配给图像视图插座的 image 属性会有所帮助。这是属性:

@IBOutlet weak var iv: UIImageView!

现在,这会编译吗?

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

不!

可选类型“UIImage?”的值必须解包为“UIImage”类型的值

好的,现在我们已经为实际用例做好了准备。


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

如果我们使用.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 键路径?关于关键路径如何与可选属性一起工作似乎有些技巧,但我不知道它是什么。


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

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 是如何工作的。看来它能够push 将管道类型的可选性up 放入.receive 运算符中。但我不明白这怎么可能。

【问题讨论】:

只是好奇:如果self.iv.image 为零,你的第一个代码会发生什么? @MartinR 它工作正常。 (在现实生活中,我会继续.store AnyCancellable,正如您所期望的那样,代码下载并显示图像就好了。)这就是我感到困惑的原因;直到我试图通过提供我自己的分配订阅者、订阅它并将其推广到 AnyCancellable(未显示)来梳理 .assign 在幕后所做的事情时,我才发现存在“问题”。 @MartinR 我会将其添加到 q 中。 到目前为止我还没有使用 Combine 的经验,所以我不“期待”任何东西 :) – 但我注意到 let pub = ... 的推断类型是 Publishers.ReceiveOn&lt;Publishers.CompactMap&lt;Publishers.ReplaceError&lt;Publishers.Map&lt;URLSession.DataTaskPublisher, Data&gt;&gt;, UIImage&gt;, DispatchQueue&gt;。如果您明确地注释它,但将UIImage 替换为UIImage?,那么错误就会消失。这似乎是编译器在第一个示例中从 .assign(to: \.image, on: self.iv) 推断的内容,因为 self.iv.image 是可选的。 如果您将compactMap 替换为map,该错误也会消失——这有意义吗?我不确定。 【参考方案1】:

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

Swift 一次推断整个语句的类型,而不是跨语句推断类型。

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

Swift 还允许类型推断自动将 (A) -&gt; B 类型的闭包提升为 (A) -&gt; 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.imageValue 类型是UIImage?,而self.iv 的类型是UIImageView!,Swift 必须做两件“自动”的事情来对这个语句进行类型检查:

它必须将闭包 UIImage(data: $0) (Data) -&gt; UIImage?类型提升为(Data) -&gt; UIImage??类型,这样compactMap就可以剥离Optional的一层并且使Output类型成为UIImage? .

它必须隐式解开iv,因为Optional&lt;UIImage&gt; 没有名为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 语句进行类型检查。不需要提升闭包类型,所以可以推导出UIImageOutput类型。

然后 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: $0) 
    .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: $0) 
    .receive(on: DispatchQueue.main)
    .subscribe(Subscribers.Assign(object: self.iv, keyPath: \.image))

Swift 类型检查成功。它与之前使用.assign 的语句基本相同。请注意,它会为 p2 推断类型 (),因为这是 .subscribe 在此处返回的内容。


现在,回到基于 keypath 的分配:

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
    

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

【讨论】:

我认为这里的关键语句可能是“Swift 不支持可选提升泛型类型参数,如输入和输出”这正是我认为 正在发生的情况,我不明白为什么会在.assign 连接东西时发生这种情况,但在我连接东西时却没有。我认为您的“促进关闭”是我正在寻找的解释。 最后四个与我在这个问题的“姐妹”问题中得出的结果平行(但不完全相同?):***.com/questions/59653363/… 事实上,我已将其附加到现有的错误报告中, bugs.swift.org/browse/SR-11184. 是的,看起来像同一个错误。 同样的提升适用于数组。如果我的闭包是() -&gt; Array&lt;Int&gt; 类型,并且我将它分配到预期() -&gt; Array&lt;Optional&lt;Int&gt;&gt; 类型的函数的位置,那是合法的,并且现在执行的闭包实际上会产生一个可选的Ints 数组。

以上是关于Swift ReferenceWritableKeyPath 如何与 Optional 属性一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

Swift入门系列--Swift官方文档(2.2)--中文翻译--About Swift 关于Swift

swift 示例BS swift.swift

swift swift_bug.swift

ios 整理(一)swift和oc的区别

swift swift_extension5.swift

swift swift_optional4.swift