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运营商。但我不明白这怎么可能。
您(马特)可能至少已经知道其中一些,但以下是其他读者的一些事实:
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.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)
Run Code Online (Sandbox Code Playgroud)
Swift 首先对语句进行类型检查let p1。它不需要提升闭包类型,因此它可以推导出 的Output类型UIImage。
然后 Swift 对语句进行类型检查let a1。它必须隐式 unwrap iv,但不需要任何Optional提升。它将Input类型推导为UIImage?因为这是Value关键路径的类型。
最后,Swift 尝试对语句进行类型检查subscribe。Output的类型是p1,UIImage的Input类型a1是UIImage?。它们是不同的,因此 Swift 无法成功地对语句进行类型检查。Swift 不支持Optional泛型类型参数的提升,例如Input和Output。所以这不能编译。
我们可以通过强制的类型来进行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)
我认为这可能是编译器中的一个错误。