准备好联合收割机



一年半以前,我唱了 RxSwift的赞歌 。 我花了一些时间才弄清楚,但是当那件事发生时,没有回头路了。 现在,我拥有世界上最好的锤子,如果我周围的一切看起来都不像钉子,该死的我。

苹果在WWDC夏季会议上介绍了Combine框架。 乍一看,它看起来像是RxSwift的更好的版本。 在我可以解释自己喜欢什么以及不喜欢什么之前,我们需要了解Combine旨在解决的问题。

反应性编程? 那又怎样


ReactiveX社区-RxSwift社区是其中的一部分-解释其本质如下:

具有可观察线程的异步编程的API。

并且:

ReactiveX是Observer和Iterator设计模式以及功能编程中最好的想法的结合。

好吧...好吧。

这到底是什么意思?

基础知识


为了真正理解反应式编程的本质,我发现了解我们如何理解它很有用。 在本文中,我将描述如何使用任何现代OOP语言查看现有类型,如何将它们扭曲,然后进行反应式编程。

在本文中,我们将快速深入丛林,这对于理解反应式编程并不是绝对必要的。

但是,我认为这是一种好奇的学术活动,特别是在强类型语言可以引导我们获得新发现方面。

因此,如果您对新细节感兴趣,请等待我的下一篇文章。

可数


我所知道的“ 反应式编程 ”起源于我曾经写过的语言-C#。 前提本身很简单:

如果它们将自己发送值给您,而不是从可枚举中提取值,该怎么办?

布莱恩·贝克曼 Brian Beckman)和埃里克·梅耶(Eric Meyer)最好地描述了 “推而不拉”的想法。 前36分钟...我什么都听不懂,但是从第36分钟开始,它变得非常有趣。

简而言之,让我们重新构建Swift中线性对象组的概念,以及可以在该线性组上迭代的对象。 您可以通过定义以下虚假的Swift协议来做到这一点:

//   ;     //    Array. protocol Enumerable { associatedtype Enum: Enumerator associatedtype Element where Self.Element == Self.Enum.Element func getEnumerator() -> Self.Enum } // ,       . protocol Enumerator: Disposable { associatedtype Element func moveNext() throws -> Bool var current: Element { get } } //          // Enumerator,         .   . protocol Disposable { func dispose() } 

双打


让我们把它翻过来,做成双打 。 我们会将数据发送到它们的来源。 并从他们离开的地方获取数据。 听起来很荒谬,但请忍受一点。

双枚举


让我们从Enumerable开始:

 //    ,  . protocol Enumerable { associatedtype Element where Self.Element == Self.Enum.Element associatedtype Enum: Enumerator func getEnumerator() -> Self.Enum } protocol DualOfEnumerable { //  Enumerator : // getEnumerator() -> Self.Enum //    : // getEnumerator(Void) -> Enumerator // //  , : // : Void; : Enumerator // getEnumerator(Void) → Enumerator // //     Void   Enumerator. //   -      Enumerator,   Void. // :  Enumerator; : Void func subscribe(DualOfEnumerator) } 

由于getEnumerator()接受Void并提供了Enumerator ,所以现在我们接受[double] Enumerator并提供Void

我知道这很奇怪。 不要离开

双枚举器


然后, DualOfEnumeratorDualOfEnumerator

 //    ,  . protocol Enumerator: Disposable { associatedtype Element // : Void; : Bool, Error func moveNext() throws -> Bool // : Void; : Element var current: Element { get } } protocol DualOfEnumerator { // : Bool, Error; : Void //   Error    func enumeratorIsDone(Bool) // : Element, : Void var nextElement: Element { set } } 

这里有几个问题:

  • Swift中没有set-only属性的概念。
  • Enumerator.moveNext() throws发生了什么?
  • Disposable怎么办?

为了解决set-only属性的问题,我们可以将其视为真正的函数。 让我们DualOfEnumerator

 protocol DualOfEnumerator { // : Bool; : Void, Error //   Error    func enumeratorIsDone(Bool) // : Element, : Void func next(Element) } 

为了解决throws的问题,让我们分离moveNext()中可能发生的错误,并将其作为单独的error()函数使用:

 protocol DualOfEnumerator { // : Bool, Error; : Void func enumeratorIsDone(Bool) func error(Error) // : Element, : Void func next(Element) } 

我们可以做其他事情:看一下迭代完成的签名:

 func enumeratorIsDone(Bool) 

随着时间的流逝,类似的事情可能会发生:

 enumeratorIsDone(false) enumeratorIsDone(false) //     enumeratorIsDone(true) 

现在,让我们简化事情,仅在一切准备就绪时才调用enumeratorIsDone 。 以此思路为指导,我们简化了代码:

 protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) } 

好好照顾自己


Disposable呢? 怎么办呢? 由于DisposableEnumerator 类型的一部分,因此当我们得到Enumerator double时 ,它可能根本就不应该在Enumerator 。 相反,它应该是DualOfEnumerable一部分。 但是到底在哪里?

DualOfEnumerator这里:

 func subscribe(DualOfEnumerator) 

如果我们接受DualOfEnumerator ,那么不应该返回 Disposable吗?

最终您会得到什么样的加倍:

 protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) } 

称它为玫瑰,虽然不是


所以,再来一次,这就是我们得到的:

 protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) } 

现在让我们来玩一下名称。

让我们从DualOfEnumerator开始。 我们将为这些功能命名更好,以更准确地描述正在发生的事情:

 protocol DualOfEnumerator { func onComplete() func onError(Error) func onNext(Element) } 

如此更好,更容易理解。

那类型名呢? 他们真可怕。 让我们对其进行一些更改。

  • DualOfEnumerator跟随线性对象组发生的事情。 可以说他观察到一个线性群。
  • DualOfEnumerable是一个观察对象。 我们在看什么。 因此,可以将其称为observable

现在进行最终更改并获得以下信息:

 protocol Observable { func subscribe(Observer)Disposable } protocol Observer { func onComplete() func onError(Error) func onNext(Element) } 


我们刚刚在RxSwift中创建了两个基本对象。 您可以在此处此处查看其真实版本。 请注意,在Observer的情况下,三个on()函数组合为一个on(Event) ,其中Event是一个枚举,用于确定事件是什么-完成,下一个值或错误。

这两种类型是RxSwift和反应式编程的基础。

关于虚假协议


我上面提到的两个“伪”协议实际上根本不是伪造的。 这些是Swift中现有类型的类似物:


那又怎样


那该担心什么呢?

在现代开发( 尤其是应用程序开发)中,异步是如此重要。 用户突然单击了一个按钮。 用户突然在UISegmentControl中选择了一个选项卡。 用户突然在UITabBar中选择了一个选项卡。 Web套接字突然为我们提供了新信息。 此下载突然-最终-结束。 此后台任务突然结束。 这个清单不胜枚举。

在现代的CocoaTouch世界中,有许多方法可以处理此类事件:

  • 通知
  • 回叫
  • 键值观察(KVO),
  • 目标/行动机制。

想象一下,是否所有这些都可以反映在一个界面中。 它可以与整个应用程序中的任何类型的异步数据或事件一起使用。

现在想象一下,如果有一整套功能可以让您修改这些 ,将它们从一种类型转换为另一种类型,从Elements中提取信息,或者甚至将它们与其他流组合。

突然,我们手中有了一套新的通用工具。
因此,我们回到了开始:

具有可观察线程的异步编程的API。

这就是使RxSwift如此强大的工具的原因。 像合并。

接下来是什么?


如果您想在实践中阅读有关RxSwift的更多信息,那么我建议您在2016年撰写五篇文章 。 他们描述了如何创建一个简单的CocoaTouch应用程序,然后逐步转换为RxSwift。

在以下文章之一中,我将解释为什么我的文章系列中针对初学者的许多技术都不适用于Combine,并且还将Combine与RxSwift进行了比较。

结合:有什么意义?


对Combine的讨论还包括对它与RxSwift之间的主要区别的讨论。 对我来说,其中三个:

  • 使用非反应性类的可能性,
  • 错误处理
  • 背压。

我将为每个项目撰写单独的文章。 我将从第一个开始。

RxCocoa的功能


在上一篇文章中,我说过RxSwift不仅仅是... RxSwift。 它为在RxCocoa的type-not-not-quite子项目中使用UIKit中的控件提供了多种可能性。 此外, RxSwiftCommunity进一步走了一步,并为UIKit的更多僻静街道以及RxSwift和RxCocoa尚未涵盖的其他一些CocoaTouch类实现了许多绑定。

因此,很容易通过单击UIButton来获得Observable流。 我将再次给出此示例:

 let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag) 

重量轻。

让我们(最后)仍然谈论组合


合并与RxSwift非常相似。 如文档所述:

Combine框架提供了一个声明性的Swift API,用于随时间处理值。

听起来很熟悉:回想一下ReactiveX(RxSwift的父项目)的描述:

具有可观察线程的异步编程的API。

在两种情况下,都说相同的话。 只是ReactiveX的描述中使用了特定的术语。 可以重新定义如下:

用于随时间推移使用值进行异步编程的API。

和我差不多。

和以前一样


当我开始分析API时,很明显,我从RxSwift知道的大多数类型在Combine中都有类似的选项:

  • 可观察→ 发布者
  • 观察者→ 订阅者
  • 一次性→ 取消 。 这是营销的胜利。 当我开始在RxSwift中描述Disposable时,您无法想象我从更公正的开发人员那里得到了多少惊喜。
  • SchedulerType→ 计划程序

到目前为止一切顺利。 再一次,我更喜欢Cancellicable,而不是Disposable。 一个很好的替代品,不仅在营销方面,而且在对对象本质的准确描述方面。

多多益善!


目前尚不清楚,但从精神上讲,它们仅用于一个目的,并且其中任何一个都不会引起错误。


“打破船尾”


一旦您开始研究RxCocoa,一切都会改变。 还记得上面的示例,我们想在其中获得一个Observable流,该流代表UIButton的点击? 这是:

 let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag) 

合并需要……要做更多的工作。

Combine不提供任何绑定到UIKit对象的功能。

这……真是个虚幻的经历。

这是使用Combine从UIControl获取UIControl.Event的常用方法:

 class ControlPublisher<T: UIControl>: Publisher { typealias ControlEvent = (control: UIControl, event: UIControl.Event) typealias Output = ControlEvent typealias Failure = Never let subject = PassthroughSubject<Output, Failure>() convenience init(control: UIControl, event: UIControl.Event) { self.init(control: control, events: [event]) } init(control: UIControl, events: [UIControl.Event]) { for event in events { control.addTarget(self, action: #selector(controlAction), for: event) } } @objc private func controlAction(sender: UIControl, forEvent event: UIControl.Event) { subject.send(ControlEvent(control: sender, event: event)) } func receive<S>(subscriber: S) where S : Subscriber, ControlPublisher.Failure == S.Failure, ControlPublisher.Output == S.Input { subject.receive(subscriber: subscriber) } } 

这里...还有更多工作。 至少呼叫看起来像:

 ControlPublisher(control: self.button, event: .touchUpInside) .sink { print("Tap!") } 

为了进行比较,RxCocoa以绑定到UIKit对象的形式提供了一种令人愉悦的可可豆:

 self.button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) 

就其本身而言,这些挑战最终确实非常相似。 唯一令人沮丧的是,我必须自己编写ControlPublisher才能达到这一点。 此外,RxSwift和RxCocoa经过了很好的测试,在项目中的使用量远远超过我的。

为了进行比较,我的ControlPublisher仅...现在出现了。 仅由于客户端数量(零)和实际使用时间(与RxCocoa相比几乎为零),我的代码才能被认为更加危险。

mm

社区帮助?


坦白地说,没有什么能阻止社区创建自己的开源“ CombineCocoa”,就像RxSwiftCommunity一样,填补了RxCocoa的空白。

但是,我认为这是Combine的巨大缺点。 我不想重写整个RxCocoa,仅是为了绑定到UIKit对象。

如果我决定下注SwiftUI ,那么我想这将消除缺乏绑定问题。 甚至我的小型应用程序也包含一堆 UI代码。 把所有这些扔出去只是为了跳上联合火车,至少是愚蠢的,甚至是危险的。

顺便说一句,“ 使用Combine接收和处理事件”文档中的文章简要介绍了如何在Combine中接收和处理事件。 简介很好,它介绍了如何从文本字段中提取值并将其保存在自定义模型对象中。 该文档还演示了如何使用运算符对所涉及的流执行一些更高级的修改。

例子


让我们继续到文档的结尾,代码示例为:

 let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .assign(to: \MyViewModel.filterString, on: myViewModel) 

我有很多问题。

通知您我不喜欢它


前两行引起我最多的问题:

 let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) 

NotificationCenter类似于应用程序总线(甚至是系统总线),其中许多人可以抛出数据或捕获经过的信息。 根据创建者的意图,此解决方案来自“万事通”类别。 实际上,在很多情况下,您可能需要找出键盘刚刚被显示或隐藏的情况。 NotificationCenter是在整个系统中分发此消息的好方法。

但是对我而言, NotificationCenter代码很扼杀 。 有时(例如获取有关键盘的通知)NotificationCenter实际上是解决该问题的最佳方法。 但是对于我来说,NotificationCenter经常是最方便的解决方案。 将某些内容放入NotificationCenter并将其拾取到应用程序中的其他位置确实非常方便。

另外,NotificationCenter是“字符串”类型的 ,也就是说,您可以很容易地犯下您尝试发布或收听的通知的错误。 Swift正在尽一切可能改善这种情况,但是仍然存在相同的NSString。

关于KVO


在Apple平台上,一直存在一种流行的方式来接收代码不同部分中的更改通知:键值观察(KVO)。 苹果这样描述:

这是一种机制,允许对象接收其他对象的指定属性更改的通知。

多亏了Gui Rambo的一条推文,我注意到Apple向Kombine添加了KVO绑定。 这可能意味着我可以摆脱对Combine中缺少RxCocoa类似物的许多失望。 如果我可以使用KVO,可以这么说,这可能会消除对CombineCocoa的需求。

我试图找出一个使用KVO从UITextField获取值并将其输出到控制台的示例:

 let sub = self.textField.publisher(for: \UITextField.text) .sink(receiveCompletion: { _ in print("Completed") }, receiveValue: { print("Text field is currently \"\($0)\"") }) 

看起来不错,继续吗?

没那么快,朋友。

我忘记了令人不安的事实

UIKit基本上与KVO不兼容。

没有KVO的支持,我的想法将行不通。 我的检查确认了这一点:当我在字段中输入文本时,代码不会向控制台输出任何内容。

因此,我希望摆脱UIKit绑定的希望是美好的,但时间不长。

清洁用品


另一个合并问题是,仍然完全不清楚在何处以及如何释放可取消对象中的资源。 似乎我们应该将它们存储在实例变量中。 但是我不记得官方文档中有关于清洁的内容。

RxSwift有一个名字叫得很方便的DisposeBag 。 在Combine中创建CancelBag并非易事,但我不太确定在这种情况下它是最佳解决方案。

在下一篇文章中,我们将讨论RxSwift和Combine中的错误处理,以及这两种方法的优缺点。

Source: https://habr.com/ru/post/zh-CN470117/


All Articles