Swift ReferenceWritableKeyPath 如何使用 Optional 属性?

mat*_*att 12 optional swift swift-keypath combine

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

@IBOutlet weak var iv: UIImageView!
Run Code Online (Sandbox Code Playgroud)

现在,这会编译吗?

    let im = UIImage()
    let kp = \UIImageView.image
    self.iv[keyPath:kp] = im // error
Run Code Online (Sandbox Code Playgroud)

不!

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

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


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

如果我们使用该.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)
Run Code Online (Sandbox Code Playgroud)

所以,我对自己说,要看看这是如何工作的,我将删除.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)
Run Code Online (Sandbox Code Playgroud)

啪!我们不能这样做,因为它UIImageView.image是一个可选的 UIImage,而我的发布者生成了一个简单明了的 UIImage。

我试图通过在关键路径中解开 Optional 来解决这个问题:

let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image!)
pub.subscribe(assign)
Run Code Online (Sandbox Code Playgroud)

酷,编译。但它在运行时崩溃,大概是因为图像视图的图像最初是nil.

现在我可以通过map在我的管道中添加一个来解决所有这些问题,该管道将 UIImage 包装在一个 Optional 中,以便所有类型都正确匹配。但我的问题是,如何这真的工作?我的意思是,为什么我不必在我使用的第一个代码中这样做.assign?为什么我可以在.image那里指定密钥路径?关于关键路径如何与 Optional 属性一起工作似乎有一些技巧,但我不知道它是什么。


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

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)
Run Code Online (Sandbox Code Playgroud)

这仍然没有解释原始的.assign工作原理。看来,它能够推动型的可选性管道进入.receive运营商。但我不明白这怎么可能。

rob*_*off 8

您(马特)可能至少已经知道其中一些,但以下是其他读者的一些事实:

  • Swift 一次推断整个语句的类型,但不会跨语句推断类型。

  • Swift 允许类型推断自动将类型的对象提升T为 type Optional<T>,如果需要对语句进行类型检查。

  • Swift 还允许类型推断自动将 type 的闭包提升(A) -> B为 type (A) -> B?。换句话说,这可以编译:

    let a: (Data) -> UIImage? = { UIImage(data: $0) }
    let b: (Data) -> UIImage?? = a
    
    Run Code Online (Sandbox Code Playgroud)

    这让我感到惊讶。我在调查你的问题时发现了这一点。

现在让我们考虑使用assign

let p0 = Just(Data())
    .compactMap { UIImage(data: $0) }
    .receive(on: DispatchQueue.main)
    .assign(to: \.image, on: self.iv)
Run Code Online (Sandbox Code Playgroud)

Swift 同时对整个语句进行类型检查。由于\UIImageView.imageValue类型是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)
Run Code Online (Sandbox Code Playgroud)

Swift 首先对语句进行类型检查let p1。它不需要提升闭包类型,因此它可以推导出 的Output类型UIImage

然后 Swift 对语句进行类型检查let a1。它必须隐式 unwrap iv,但不需要任何Optional提升。它将Input类型推导为UIImage?因为这是Value关键路径的类型。

最后,Swift 尝试对语句进行类型检查subscribeOutput的类型是p1UIImageInput类型a1UIImage?。它们是不同的,因此 Swift 无法成功地对语句进行类型检查。Swift 不支持Optional泛型类型参数的提升,例如InputOutput。所以这不能编译。

我们可以通过强制的类型来进行Output类型检查:p1UIImage?

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)
Run Code Online (Sandbox Code Playgroud)

在这里,我们强制 Swift 提升闭包类型。我使用了eraseToAnyPublisherelse 的类型,因为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))
Run Code Online (Sandbox Code Playgroud)

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


现在,回到基于键路径的分配:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv[keyPath: kp] = im
    }
}
Run Code Online (Sandbox Code Playgroud)

这无法编译,并出现错误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)
    }
}
Run Code Online (Sandbox Code Playgroud)

iv如果我们更改to的类型UIImageView?并可选化赋值,它也会编译:

class Thing {
    var iv: UIImageView? = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv?[keyPath: kp] = im
    }
}
Run Code Online (Sandbox Code Playgroud)

但如果我们只是强制解包隐式解包的可选选项,它就不会编译:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv![keyPath: kp] = im
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我们只是选择赋值,它就不会编译:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv?[keyPath: kp] = im
    }
}
Run Code Online (Sandbox Code Playgroud)

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