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.iv
的 image
属性。
如果我们使用.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<Publishers.CompactMap<Publishers.ReplaceError<Publishers.Map<URLSession.DataTaskPublisher, Data>>, UIImage>, DispatchQueue>
。如果您明确地注释它,但将UIImage
替换为UIImage?
,那么错误就会消失。这似乎是编译器在第一个示例中从 .assign(to: \.image, on: self.iv)
推断的内容,因为 self.iv.image
是可选的。
如果您将compactMap
替换为map
,该错误也会消失——这有意义吗?我不确定。
【参考方案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
语句进行类型检查。不需要提升闭包类型,所以可以推导出UIImage
的Output
类型。
然后 Swift 对 let a1
语句进行类型检查。它必须隐式打开 iv
,但不需要任何 Optional
提升。它将Input
类型推断为UIImage?
,因为这是密钥路径的Value
类型。
最后,Swift 尝试对subscribe
语句进行类型检查。 p1
的Output
类型是UIImage
,a1
的Input
类型是UIImage?
。它们是不同的,因此 Swift 无法成功地对语句进行类型检查。 Swift 不支持Optional
提升泛型类型参数,如Input
和Output
。所以这不会编译。
我们可以通过将p1
的Output
类型强制为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.
是的,看起来像同一个错误。
同样的提升适用于数组。如果我的闭包是() -> Array<Int>
类型,并且我将它分配到预期() -> Array<Optional<Int>>
类型的函数的位置,那是合法的,并且现在执行的闭包实际上会产生一个可选的Ints 数组。以上是关于Swift ReferenceWritableKeyPath 如何与 Optional 属性一起使用?的主要内容,如果未能解决你的问题,请参考以下文章