Vorbereitungen für das Kombinieren



Vor anderthalb Jahren habe ich RxSwift gelobt . Ich brauchte eine Weile, um es herauszufinden, aber als das passierte, gab es kein Zurück mehr. Jetzt hatte ich den besten Hammer der Welt und verdammt noch mal, wenn nicht alles um mich herum wie ein Nagel aussah.

Apple stellte das Combine Framework auf der WWDC Summer Conference vor. Auf den ersten Blick sieht es nach einer etwas besseren Version von RxSwift aus. Bevor ich erklären kann, was mir daran gefällt und was nicht, müssen wir verstehen, welches Problem Combine lösen soll.

Reaktive Programmierung? Na und?


Die ReactiveX-Community - zu der auch die RxSwift-Community gehört - erklärt ihre Essenz wie folgt:

API für asynchrone Programmierung mit beobachtbaren Threads.

Und auch:

ReactiveX ist eine Kombination der besten Ideen aus den Entwurfsmustern Observer und Iterator sowie der funktionalen Programmierung.

Nun ... okay.

Und was bedeutet das wirklich ?

Die Grundlagen


Um die Essenz der reaktiven Programmierung wirklich zu verstehen, finde ich es nützlich zu verstehen, wie wir dazu gekommen sind. In diesem Artikel werde ich beschreiben, wie Sie vorhandene Typen in jeder modernen OOP-Sprache betrachten, verdrehen und dann zur reaktiven Programmierung gelangen können.

In diesem Artikel werden wir uns schnell mit dem Dschungel befassen, was für das Verständnis der reaktiven Programmierung nicht unbedingt erforderlich ist.

Ich halte dies jedoch für eine merkwürdige akademische Übung, insbesondere im Hinblick darauf, wie stark typisierte Sprachen uns zu neuen Entdeckungen führen können.

Warten Sie also auf meine nächsten Beiträge, wenn Sie an neuen Details interessiert sind.

Aufzählbar


Die mir bekannte „ reaktive Programmierung “ stammt aus der Sprache, in der ich einmal geschrieben habe - C #. Die Prämisse selbst ist ganz einfach:

Was ist, wenn sie Ihnen die Werte selbst senden, anstatt Werte aus der Aufzählung zu extrahieren?

Diese Idee, "drücken statt ziehen", wurde am besten von Brian Beckman und Eric Meyer beschrieben. Die ersten 36 Minuten ... Ich habe nichts verstanden, aber ab der 36. Minute wird es wirklich interessant.

Kurz gesagt, lassen Sie uns die Idee einer linearen Gruppe von Objekten in Swift sowie eines Objekts, das über diese lineare Gruppe iterieren kann, neu formulieren. Sie können dies tun, indem Sie diese gefälschten Swift-Protokolle definieren:

//   ;     //    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() } 

Doppel


Lassen Sie uns alles umdrehen und Doppel machen. Wir senden Daten dorthin, woher sie stammen. Und holen Sie sich die Daten von dort, wo sie abgereist sind. Es klingt absurd, aber ertrage es ein wenig.

Doppelte Aufzählung


Beginnen wir mit 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) } 

Da getEnumerator() Void nahm und Enumerator gab, akzeptieren wir jetzt den [double] Enumerator und geben Void .

Ich weiß, dass das seltsam ist. Geh nicht weg.

Doppelter Enumerator


Und was ist dann DualOfEnumerator ?

 //    ,  . 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 } } 

Hier gibt es mehrere Probleme:

  • In Swift gibt es kein Konzept für eine Nur-Set-Eigenschaft.
  • Was ist mit throws in Enumerator.moveNext() passiert?
  • Was passiert mit Disposable ?

Um das Problem mit der Eigenschaft set-only zu beheben, können wir es als das behandeln, was es wirklich ist - eine Funktion. Lassen Sie uns unseren DualOfEnumerator :

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

Um das Problem mit throws zu lösen, trennen wir den Fehler, der in moveNext() und arbeiten damit als separate error() -Funktion:

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

Wir können noch etwas anderes tun: Sehen Sie sich die Signatur des Abschlusses der Iteration an:

 func enumeratorIsDone(Bool) 

Wahrscheinlich wird im Laufe der Zeit etwas Ähnliches passieren:

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

Lassen Sie uns nun die Dinge vereinfachen und enumeratorIsDone nur aufrufen, wenn ... alles wirklich fertig ist. Anhand dieser Idee vereinfachen wir den Code:

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

Pass auf uns auf


Was ist mit Disposable ? Was soll man damit machen? Da Disposable Teil des Enumerator Typs ist , sollte der Enumerator , wenn wir ihn doppelt erhalten , wahrscheinlich überhaupt nicht im Enumerator . Stattdessen sollte es Teil von DualOfEnumerable . Aber wo genau?

DualOfEnumerator hier ein:

 func subscribe(DualOfEnumerator) 

Wenn wir DualOfEnumerator akzeptieren, sollte Disposable dann nicht zurückgegeben werden ?

Hier ist, was für ein Double du hast:

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

Nennen wir es eine Rose, aber nicht


Also, noch einmal, hier ist was wir haben:

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

Lassen Sie uns jetzt ein wenig mit den Namen spielen.

Beginnen wir mit DualOfEnumerator . Wir werden bessere Namen für die Funktionen finden, um genauer zu beschreiben, was passiert:

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

So viel besser und verständlicher.

Was ist mit Typnamen? Sie sind einfach schrecklich. Lassen Sie uns sie ein wenig ändern.

  • DualOfEnumerator - etwas, das folgt, was mit einer linearen Gruppe von Objekten passiert. Wir können sagen, dass er eine lineare Gruppe beobachtet .
  • DualOfEnumerable ist Gegenstand der Beobachtung. Was wir sehen. Daher kann es als beobachtbar bezeichnet werden .

Nehmen Sie nun die letzten Änderungen vor und erhalten Sie Folgendes:

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

Wow


Wir haben gerade zwei grundlegende Objekte in RxSwift erstellt. Sie können ihre realen Versionen hier und hier sehen . Beachten Sie, dass im Fall von Observer die drei on() -Funktionen zu einer on(Event) kombiniert werden, wobei Event eine Aufzählung ist, die bestimmt, was das Ereignis ist - Abschluss, nächster Wert oder Fehler.

Diese beiden Typen liegen RxSwift und der reaktiven Programmierung zugrunde.

Über gefälschte Protokolle


Die beiden oben erwähnten "gefälschten" Protokolle sind eigentlich überhaupt nicht gefälscht. Dies sind Analoga bestehender Typen in Swift:


Na und?


Worüber sollten Sie sich also Sorgen machen?

So viel in der modernen Entwicklung - insbesondere in der Anwendungsentwicklung - ist mit Asynchronität verbunden. Der Benutzer klickte plötzlich auf eine Schaltfläche. Der Benutzer hat plötzlich eine Registerkarte im UISegmentControl ausgewählt. Der Benutzer hat plötzlich eine Registerkarte in der UITabBar ausgewählt. Der Web-Socket gab uns plötzlich neue Informationen. Dieser Download endete plötzlich - und schließlich -. Diese Hintergrundaufgabe endete abrupt. Diese Liste geht weiter und weiter.

In der modernen CocoaTouch-Welt gibt es viele Möglichkeiten, mit solchen Ereignissen umzugehen:

  • Benachrichtigungen
  • Rückrufe
  • Schlüsselwertbeobachtung (KVO),
  • Ziel- / Aktionsmechanismus.

Stellen Sie sich vor, alles könnte sich in einer einzigen Schnittstelle widerspiegeln. Welches mit jeder Art von asynchronen Daten oder Ereignissen innerhalb der gesamten Anwendung arbeiten könnte.

Stellen Sie sich nun vor, es gäbe eine ganze Reihe von Funktionen , mit denen Sie diese Streams ändern, von einem Typ in einen anderen konvertieren, Informationen aus Elementen extrahieren oder sie sogar mit anderen Streams kombinieren können.

Plötzlich liegt in unseren Händen ein neues universelles Werkzeugset.
Und so kehrten wir zum Anfang zurück:

API für asynchrone Programmierung mit beobachtbaren Threads.

Dies macht RxSwift zu einem so leistungsstarken Tool. Wie kombinieren.

Was weiter?


Wenn Sie mehr über RxSwift in der Praxis lesen möchten, empfehle ich meine fünf Artikel aus dem Jahr 2016 . Sie beschreiben die Erstellung einer einfachen CocoaTouch-Anwendung, gefolgt von einer schrittweisen Konvertierung in RxSwift.

In einem der folgenden Artikel werde ich erklären, warum viele der in meiner Artikelserie für Anfänger beschriebenen Techniken in Combine nicht anwendbar sind, und ich vergleiche Combine mit RxSwift.

Kombinieren: Was ist der Sinn?


Die Diskussion von Combine beinhaltet auch eine Diskussion der Hauptunterschiede zwischen ihm und RxSwift. Für mich gibt es drei davon:

  • die Möglichkeit der Verwendung nicht reaktiver Klassen,
  • Fehlerbehandlung
  • Gegendruck.

Ich werde jedem Artikel einen eigenen Artikel widmen. Ich werde mit dem ersten beginnen.

Eigenschaften von RxCocoa


In einem früheren Beitrag habe ich gesagt, dass RxSwift mehr als ... RxSwift ist. Es bietet zahlreiche Möglichkeiten für die Verwendung von Steuerelementen von UIKit im Typ-aber-nicht-ganz-Teilprojekt von RxCocoa. Darüber hinaus ging RxSwiftCommunity noch einen Schritt weiter und implementierte viele Bindungen für noch abgelegenere Nebenstraßen von UIKit sowie einige andere CocoaTouch-Klassen, die RxSwift und RxCocoa noch nicht abdecken.

Daher ist es sehr einfach, einen Observable Stream zu erhalten, indem Sie beispielsweise auf UIButton klicken. Ich werde dieses Beispiel noch einmal geben:

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

Leichtgewichtler.

Lassen Sie uns (endlich) noch über Kombinieren sprechen


Combine ist RxSwift sehr ähnlich. Wie die Dokumentation sagt:

Das Combine-Framework bietet eine deklarative Swift-API für die Verarbeitung von Werten im Zeitverlauf.

Klingt vertraut: Erinnern Sie sich an die Beschreibung von ReactiveX (dem übergeordneten Projekt für RxSwift):

API für asynchrone Programmierung mit beobachtbaren Threads.

In beiden Fällen wird dasselbe gesagt. Es ist nur so, dass in der Beschreibung von ReactiveX bestimmte Begriffe verwendet werden. Es kann wie folgt umformuliert werden:

Eine API für die asynchrone Programmierung mit Werten über die Zeit.

Fast das gleiche wie bei mir.

Gleich wie zuvor


Als ich mit der Analyse der API begann, wurde sofort klar, dass die meisten Typen, die ich von RxSwift kenne, ähnliche Optionen in Combine haben:

  • Beobachtbar → Verlag
  • Beobachter → Abonnent
  • Einweg → Stornierbar . Dies ist ein Triumph des Marketings. Sie können sich nicht vorstellen, wie viele überraschte Blicke ich von unvoreingenommeneren Entwicklern erhalten habe, als ich anfing, Disposable in RxSwift zu beschreiben.
  • SchedulerType → Scheduler

So weit so gut. Ich mag Cancellable wieder viel mehr als Disposable. Ein großartiger Ersatz, nicht nur in Bezug auf Marketing, sondern auch in Bezug auf eine genaue Beschreibung des Wesens des Objekts.

Mehr ist noch besser!


Dies ist nicht sofort klar, aber geistig dienen sie einem Zweck, und keiner von ihnen kann zu Fehlern führen.


"Pause für Kacke"


Alles ändert sich, sobald Sie sich mit RxCocoa beschäftigen. Erinnern Sie sich an das obige Beispiel, in dem wir einen Observable-Stream erhalten wollten, der Klicks auf UIButton darstellt? Da ist er:

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

Kombinieren erfordert ... viel mehr Arbeit, um dasselbe zu tun.

Combine bietet keine Funktionen zum Binden an UIKit-Objekte.

Das ist ... nur ein unwirklicher Mist.

Hier ist eine übliche Methode, um UIControl.Event mithilfe von Combine von UIControl abzurufen :

 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) } } 

Hier ... viel mehr Arbeit. Zumindest sieht der Anruf so aus:

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

Zum Vergleich: RxCocoa bietet einen angenehmen, leckeren Kakao in Form von Bindungen an UIKit-Objekte:

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

An sich sind diese Herausforderungen letztendlich wirklich sehr ähnlich. Das einzig Frustrierende ist, dass ich ControlPublisher selbst schreiben musste, um an diesen Punkt zu gelangen. Darüber hinaus sind RxSwift und RxCocoa sehr gut getestet und werden in Projekten viel mehr als in meinen verwendet.

Zum Vergleich erschien mein ControlPublisher nur ... jetzt. Nur aufgrund der Anzahl der Clients (Null) und der Nutzungszeit in der realen Welt (fast Null im Vergleich zu RxCocoa) kann mein Code als unendlich gefährlicher angesehen werden.

Schade.

Gemeinschaftshilfe?


Ehrlich gesagt hindert nichts die Community daran, ein eigenes Open-Source-Programm „CombineCocoa“ zu erstellen, das die RxCocoa-Lücke genau wie die RxSwiftCommunity füllen würde.

Ich halte dies jedoch für einen großen Nachteil von Combine. Ich möchte nicht den gesamten RxCocoa neu schreiben, sondern nur Bindungen an UIKit-Objekte erhalten.

Wenn ich mich für SwiftUI entscheide, wird das Problem des Mangels an Bindungen wahrscheinlich dadurch beseitigt . Sogar meine kleine Anwendung enthält eine Menge UI-Code. Das alles rauszuwerfen, nur um in den Kombinationszug zu springen, wäre zumindest dumm oder sogar gefährlich.

Übrigens beschreibt der Artikel in der Dokumentation zum Empfangen und Behandeln von Ereignissen mit Kombinieren kurz, wie Ereignisse in Kombinieren empfangen und verarbeitet werden. Die Einführung ist gut. Sie zeigt, wie Sie einen Wert aus einem Textfeld extrahieren und in einem benutzerdefinierten Modellobjekt speichern. Die Dokumentation zeigt auch die Verwendung von Operatoren, um einige erweiterte Änderungen an dem betreffenden Stream vorzunehmen.

Beispiel


Fahren wir mit dem Ende der Dokumentation fort, wo das Codebeispiel lautet:

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

Ich habe ... viele Probleme damit.

Benachrichtigen Sie, dass es mir nicht gefällt


Die ersten beiden Zeilen werfen mir die meisten Fragen auf:

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

NotificationCenter ist so etwas wie ein Anwendungsbus (oder sogar ein Systembus), in den viele Daten werfen oder vorbeifliegende Informationen abfangen können. Diese Lösung gehört zur Kategorie "Alles für alle", wie von den Erstellern beabsichtigt. Und es gibt wirklich viele Situationen, in denen Sie möglicherweise herausfinden müssen, ob die Tastatur nur ein- oder ausgeblendet wurde. NotificationCenter ist eine großartige Möglichkeit, diese Nachricht im gesamten System zu verteilen.

Aber für mich ist NotificationCenter Code mit einer Drossel . Es gibt Zeiten (wie das Erhalten einer Benachrichtigung über die Tastatur), in denen NotificationCenter tatsächlich die bestmögliche Lösung für das Problem ist. Aber zu oft ist NotificationCenter für mich die bequemste Lösung. Es ist wirklich sehr praktisch, etwas in NotificationCenter abzulegen und es an einer anderen Stelle in der Anwendung abzurufen.

Darüber hinaus ist das NotificationCenter vom Typ "Zeichenfolge", dh Sie können leicht den Fehler machen, welche Benachrichtigung Sie veröffentlichen oder anhören möchten . Swift tut alles, um die Situation zu verbessern, aber immer noch liegt derselbe NSString unter der Haube.

Über KVO


Auf der Apple-Plattform gibt es seit langem eine beliebte Möglichkeit, Benachrichtigungen über Änderungen in verschiedenen Teilen des Codes zu erhalten: Key-Value-Observation (KVO). Apple beschreibt es so:

Dies ist ein Mechanismus, mit dem Objekte Benachrichtigungen über Änderungen an den angegebenen Eigenschaften anderer Objekte erhalten können.

Dank eines Gui Rambo-Tweets bemerkte ich, dass Apple Combine KVO-Bindungen hinzufügte. Dies könnte bedeuten, dass ich die vielen Enttäuschungen über das Fehlen eines RxCocoa-Analogons in Combine loswerden könnte. Wenn ich KVO verwenden kann, wird dies wahrscheinlich sozusagen die Notwendigkeit von CombineCocoa beseitigen.

Ich habe versucht, ein Beispiel für die Verwendung von KVO zu finden, um einen Wert von einem UITextField und an die Konsole auszugeben:

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

Sieht gut aus, mach weiter?

Nicht so schnell, Freunde.

Ich habe die unangenehme Wahrheit vergessen:

UIKit ist im Großen und Ganzen nicht mit KVO kompatibel.

Und ohne KVO-Unterstützung wird meine Idee nicht funktionieren. Meine Überprüfungen haben dies bestätigt: Der Code gibt nichts an die Konsole aus, wenn ich Text in das Feld eingebe.

Meine Hoffnungen, die Notwendigkeit von UIKit-Bindungen loszuwerden, waren wunderschön, aber nicht lange.

Reinigung


Ein weiteres Kombinationsproblem besteht darin, dass noch völlig unklar ist, wo und wie Ressourcen in stornierbaren Objekten freigegeben werden sollen. Es scheint, dass wir sie in Instanzvariablen speichern sollten. Aber ich erinnere mich nicht, dass in der offiziellen Dokumentation etwas über die Reinigung gesagt wurde.

RxSwift hat eine schrecklich benannte, aber unglaublich praktische DisposeBag . Es ist nicht weniger einfach, CancelBag in Combine zu erstellen, aber ich bin mir nicht ganz sicher, ob es in diesem Fall die beste Lösung ist.

Im nächsten Artikel werden wir über die Fehlerbehandlung in RxSwift und Combine sprechen, über die Vor- und Nachteile beider Ansätze.

Source: https://habr.com/ru/post/de470117/


All Articles