Swift ReferenceWritableKeyPath如何与Optional属性一起使用?
生存之地:在阅读之前了解一下您无法通过键路径\UIImageView.image
将UIImage分配给图像视图插座的image
属性将很有帮助.这是属性:
Ground of Being: It will help, before reading, to know that you cannot assign a UIImage to an image view outlet's image
property through the keypath \UIImageView.image
. Here's the property:
@IBOutlet weak var iv: UIImageView!
现在,可以编译吗?
let im = UIImage()
let kp = \UIImageView.image
self.iv[keyPath:kp] = im // error
不!
可选类型'UIImage?'的值必须解包为'UIImage'类型的值
Value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'
好的,现在我们可以开始实际使用了.
Okay, now we're ready for the actual use case.
我实际上想了解的是Combine框架.assign
订阅服务器在幕后的工作方式.为了进行实验,我尝试使用自己的Assign对象.在我的示例中,我的发布者管道生成一个UIImage对象,并将其分配给UIImageView属性self.iv
的image
属性.
What I'm actually trying to understand is how the Combine framework .assign
subscriber works behind the scenes. To experiment, I tried using my own Assign object. In my example, my publisher pipeline produces a UIImage object, and I assign it to the image
property of a UIImageView property self.iv
.
如果我们使用.assign
方法,它将编译并运行:
If we use the .assign
method, this compiles and works:
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对象:
So, says I to myself, to see how this works, I'll remove the .assign
and replace it with my own Assign object:
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简单明了.
Blap! We can't do that, because UIImageView.image
is an Optional UIImage, and my publisher produces a UIImage plain and simple.
我试图通过在关键路径中展开Optional来解决此问题:
I tried to work around this by unwrapping the Optional in the key path:
let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image!)
pub.subscribe(assign)
很酷,可以编译.但是它在运行时崩溃,可能是因为图像视图的图像最初是nil
.
Cool, that compiles. But it crashes at runtime, presumably because the image view's image is initially nil
.
现在,我可以通过在管道中添加map
来解决所有这些问题,该管道将UIImage包裹在Optional中,以便所有类型正确匹配.但是我的问题是,这真的是如何工作的?我的意思是,为什么我不必在使用.assign
的第一个代码中这样做?为什么我可以在那里指定.image
键路径?关键路径如何与Optional属性一起使用似乎有些技巧,但我不知道它是什么.
Now I can work around all of this just fine by adding a map
to my pipeline that wraps the UIImage up in an Optional, so that all the types match correctly. But my question is, how does this really work? I mean, why don't I have to do that in the first code where I use .assign
? Why am I able to specify the .image
keypath there? There seems to be some trickery about how key paths work with Optional properties but I don't know what it is.
在Martin R的一些输入之后,我意识到,如果在生成UIImage?
时显式地键入pub
,则会得到与添加map
相同的效果,该map
将UIImage包裹在Optional中.这样就可以编译工作了
After some input from Martin R I realized that if we type pub
explicitly as producing UIImage?
we get the same effect as adding a map
that wraps the UIImage in an Optional. So this compiles and works
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
的工作方式.看来,它可以将类型 up 的可选内容推送到.receive
运算符中.但是我不知道怎么可能.
This still doesn't explain how the original .assign
works. It appears that it is able to push the optionality of the type up the pipeline into the .receive
operator. But I don't see how that is possible.
您(马特)可能至少已经知道其中一些内容,但这是其他读者的一些事实:
You (Matt) probably know at least some of this already, but here are some facts for other readers:
-
Swift一次推断一个完整的语句的类型,而不是跨语句的推断.
Swift infers types on one whole statement at a time, but not across statements.
Swift允许类型推断自动将类型为T
的对象提升为类型为Optional<T>
,如果需要进行语句类型检查.
Swift allows type inference to automatically promote an object of type T
to type Optional<T>
, if necessary to make the statement type-check.
Swift还允许类型推断自动将类型为(A) -> B
的闭包提升为类型为(A) -> B?
.换句话说,它将编译为:
Swift also allows type inference to automatically promote a closure of type (A) -> B
to type (A) -> B?
. In other words, this compiles:
let a: (Data) -> UIImage? = { UIImage(data: $0) }
let b: (Data) -> UIImage?? = a
这让我感到惊讶.我是在调查您的问题时发现它的.
This came as a surprise to me. I discovered it while investigating your problem.
现在让我们考虑使用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必须做两项自动"操作来使该语句进行类型检查:
Swift type-checks this entire statement simultaneously. Since \UIImageView.image
's Value
type is UIImage?
, and self.iv
's type is UIImageView!
, Swift has to do two "automatic" things to make this statement type-check:
-
必须将闭包
{ UIImage(data: $0) }
从类型(Data) -> UIImage?
升级为类型(Data) -> UIImage??
,以便compactMap
可以剥离Optional
的一级并将Output
类型设置为UIImage?
.
It has to promote the closure
{ UIImage(data: $0) }
from type(Data) -> UIImage?
to type(Data) -> UIImage??
so thatcompactMap
can strip off one level ofOptional
and make theOutput
type beUIImage?
.
它必须隐式解包iv
,因为Optional<UIImage>
没有名为image
的属性,但是UIImage
有.
It has to implicitly unwrap iv
, because Optional<UIImage>
has no property named image
, but UIImage
does.
这两个动作使Swift可以成功地对语句进行类型检查.
These two actions let Swift type-check the statement successfully.
现在假设我们将其分为三个语句:
Now suppose we break it into three statements:
let p1 = Just(Data())
.compactMap { UIImage(data: $0) }
.receive(on: DispatchQueue.main)
let a1 = Subscribers.Assign(object: self.iv, keyPath: \.image)
p1.subscribe(a1)
首先进行快速类型检查let p1
语句.它不需要提升闭包类型,因此可以推断出Output
类型的UIImage
.
Swift first type-checks the let p1
statement. It has no need to promote the closure type, so it can deduce an Output
type of UIImage
.
然后,Swift对let a1
语句进行类型检查.它必须隐式解开iv
,但是不需要任何Optional
升级.它将Input
类型推导为UIImage?
,因为这是密钥路径的Value
类型.
Then Swift type-checks the let a1
statement. It must implicitly unwrap iv
, but there's no need for any Optional
promotion. It deduces the Input
type as UIImage?
because that is the Value
type of the key path.
最后,Swift尝试对subscribe
语句进行类型检查. p1
的Output
类型是UIImage
,而a1
的Input
类型是UIImage?
.这些是不同的,因此Swift无法成功对语句进行类型检查. Swift不支持Optional
推广通用类型参数,例如Input
和Output
.所以这不会编译.
Finally, Swift tries to type-check the subscribe
statement. The Output
type of p1
is UIImage
, and the Input
type of a1
is UIImage?
. These are different, so Swift cannot type-check the statement successfully. Swift does not support Optional
promotion of generic type parameters like Input
and Output
. So this doesn't compile.
我们可以通过强制p1
的Output
类型为UIImage?
来进行类型检查:
We can make this type-check by forcing the Output
type of p1
to be 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
的类型太难看了,无法说明.
Here, we force Swift to promote the closure type. I used eraseToAnyPublisher
because otherwise p1
's type is too ugly to spell out.
由于Subscribers.Assign.init
是公开的,所以我们也可以直接使用它来使Swift推断所有类型:
Since Subscribers.Assign.init
is public, we can also use it directly to make Swift infer all the types:
let p2 = Just(Data())
.compactMap { UIImage(data: $0) }
.receive(on: DispatchQueue.main)
.subscribe(Subscribers.Assign(object: self.iv, keyPath: \.image))
Swift成功进行类型检查.它与先前使用.assign
的语句基本相同.请注意,它会推断p2
的类型()
,因为这是.subscribe
返回的内容.
Swift type-checks this successfully. It is essentially the same as the statement that used .assign
earlier. Note that it infers type ()
for p2
because that's what .subscribe
returns here.
现在,回到基于键路径的分配:
Now, back to your keypath-based assignment:
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?
:
This doesn't compile, with the error value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'
. I don't know why Swift can't compile this. It compiles if we explicitly convert im
to UIImage?
:
class Thing {
var iv: UIImageView! = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv[keyPath: kp] = .some(im)
}
}
如果我们将iv
的类型更改为UIImageView?
并可选地分配赋值,它也会进行编译:
It also compiles if we change the type of iv
to UIImageView?
and optionalize the assignment:
class Thing {
var iv: UIImageView? = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv?[keyPath: kp] = im
}
}
但是如果我们只强行解开隐式解开的可选内容,则它不会编译:
But it does not compile if we just force-unwrap the implicitly-unwrapped optional:
class Thing {
var iv: UIImageView! = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv![keyPath: kp] = im
}
}
如果我们仅对分配进行可选设置,它就不会编译:
And it does not compile if we just optionalize the assignment:
class Thing {
var iv: UIImageView! = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv?[keyPath: kp] = im
}
}
我认为这可能是编译器中的错误.
I think this might be a bug in the compiler.