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.image
的 Value
类型是 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
语句进行类型检查。 p1
的Output
类型是UIImage
,a1
的Input
类型是UIImage?
。它们是不同的,因此 Swift 无法成功地对语句进行类型检查。 Swift 不支持 Optional
提升泛型类型参数,如 Input
和 Output
。所以这不会编译。
我们可以通过强制 p1
的 Output
类型为 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
}
}
我认为这可能是编译器中的错误。
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.image
的 Value
类型是 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
语句进行类型检查。 p1
的Output
类型是UIImage
,a1
的Input
类型是UIImage?
。它们是不同的,因此 Swift 无法成功地对语句进行类型检查。 Swift 不支持 Optional
提升泛型类型参数,如 Input
和 Output
。所以这不会编译。
我们可以通过强制 p1
的 Output
类型为 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
}
}
我认为这可能是编译器中的错误。