Swift ReferenceWritableKeyPath如何与Optional属性一起使用? [英] How does Swift ReferenceWritableKeyPath work with an Optional property?

查看:556
本文介绍了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.ivimage属性.

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.imageValue类型为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 that compactMap can strip off one level of Optional and make the Output type be UIImage?.

它必须隐式解包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语句进行类型检查. p1Output类型是UIImage,而a1Input类型是UIImage?.这些是不同的,因此Swift无法成功对语句进行类型检查. Swift不支持Optional推广通用类型参数,例如InputOutput.所以这不会编译.

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.

我们可以通过强制p1Output类型为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.

这篇关于Swift ReferenceWritableKeyPath如何与Optional属性一起使用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆