In Objective-C sehe ich zwei gebräuchliche Muster für Blöcke. Eines ist ein Paar von Erfolg: / Misserfolg: Blöcke, das andere ist eine einzelne Vervollständigung: Block.
Angenommen, ich habe eine Aufgabe, die ein Objekt asynchron zurückgibt, und diese Aufgabe schlägt möglicherweise fehl. Das erste Muster ist -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure
. Das zweite Muster ist -taskWithCompletion:(void (^)(id object, NSError *error))completion
.
Erfolg: / Misserfolg:
[target taskWithSuccess:^(id object) {
// W00t! I've got my object
} failure:^(NSError *error) {
// Oh noes! report the failure.
}];
Fertigstellung:
[target taskWithCompletion:^(id object, NSError *error) {
if (object) {
// W00t! I've got my object
} else {
// Oh noes! report the failure.
}
}];
Welches ist das bevorzugte Muster? Was sind die Stärken und Schwächen? Wann würden Sie eins übereinander verwenden?
design-patterns
objective-c
Jeffery Thomas
quelle
quelle
Antworten:
Der Abschluss-Rückruf (im Gegensatz zum Erfolg / Misserfolg-Paar) ist allgemeiner. Wenn Sie einen Kontext vorbereiten müssen, bevor Sie sich mit dem Rückgabestatus befassen, können Sie dies direkt vor der "if (object)" - Klausel tun. Bei Erfolg / Misserfolg müssen Sie diesen Code duplizieren. Dies hängt natürlich von der Callback-Semantik ab.
quelle
-task…
das Objekt zurückgegeben werden könnte, sich das Objekt jedoch nicht im richtigen Zustand befindet, müsste in der Erfolgsbedingung dennoch eine Fehlerbehandlung durchgeführt werden.Ich würde sagen, ob die API einen Completion-Handler oder ein Paar Erfolgs- / Fehlerblöcke bereitstellt , ist in erster Linie eine Frage der persönlichen Präferenz.
Beide Ansätze haben Vor- und Nachteile, obwohl es nur geringfügige Unterschiede gibt.
Bedenken Sie, dass es auch weitere Varianten gibt, zum Beispiel, bei denen der eine Completion-Handler möglicherweise nur einen Parameter hat , der das mögliche Ergebnis oder einen möglichen Fehler kombiniert :
Der Zweck dieser Signatur besteht darin, dass ein Completion-Handler generisch in anderen APIs verwendet werden kann.
Zum Beispiel gibt es in Category for NSArray eine Methode,
forEachApplyTask:completion:
die nacheinander eine Aufgabe für jedes Objekt aufruft und die Schleife unterbricht, wenn ein Fehler aufgetreten ist . Da diese Methode selbst ebenfalls asynchron ist, verfügt sie auch über einen Completion-Handler:In der Tat ist
completion_t
wie oben definiert generisch genug und ausreichend, um alle Szenarien zu behandeln.Es gibt jedoch andere Mittel, mit denen eine asynchrone Task ihre Abschlussbenachrichtigung an die Aufrufstelle signalisiert:
Versprechen
Versprechen, auch „Futures“, „Latente“ oder „Delayed“ stellen die genannten eventuellen Ergebnis einer asynchronen Aufgabe (siehe auch: Wiki Futures und Versprechen ).
Zu Beginn befindet sich ein Versprechen im Status "Ausstehend". Das heißt, sein "Wert" ist noch nicht bewertet und noch nicht verfügbar.
In Objective-C wäre ein Promise ein gewöhnliches Objekt, das von einer asynchronen Methode wie folgt zurückgegeben wird:
In der Zwischenzeit beginnen die asynchronen Tasks mit der Auswertung ihres Ergebnisses.
Beachten Sie auch, dass es keinen Completion-Handler gibt. Stattdessen wird das Versprechen ein leistungsfähigeres Mittel bieten, mit dem die anrufende Site das spätere Ergebnis der asynchronen Aufgabe erhalten kann, die wir bald sehen werden.
Die asynchrone Aufgabe, die das Versprechungsobjekt erstellt hat, MUSS schließlich ihr Versprechen „auflösen“. Das heißt, da eine Aufgabe entweder erfolgreich sein oder fehlschlagen kann, MUSS sie entweder ein Versprechen erfüllen, das das bewertete Ergebnis enthält, oder das Versprechen ablehnen, das ein Fehler ist, der den Grund für den Fehler angibt.
Wenn ein Versprechen aufgelöst wurde, kann es seinen Status, einschließlich seines Werts, nicht mehr ändern.
Sobald ein Versprechen gelöst wurde, kann eine Call-Site das Ergebnis erhalten (ob es fehlgeschlagen oder erfolgreich war). Wie dies erreicht wird, hängt davon ab, ob das Versprechen im synchronen oder im asynchronen Stil implementiert wird.
A versprechen kann in einem synchronen oder einem asynchronen Art was zu entweder implementiert wird , blockieren bzw. nicht blockierenden Semantik.
Um den Wert des Versprechens abzurufen, würde eine Call-Site in einem synchronen Stil eine Methode verwenden, die den aktuellen Thread blockiert, bis das Versprechen durch die asynchrone Task aufgelöst wurde und das endgültige Ergebnis verfügbar ist.
In einem asynchronen Stil würde die Call-Site Callbacks oder Handler-Blöcke registrieren, die unmittelbar nach dem Erledigen des Versprechens aufgerufen werden.
Es stellte sich heraus, dass der synchrone Stil eine Reihe von erheblichen Nachteilen aufweist, die die Vorzüge asynchroner Aufgaben effektiv zunichte machen. Ein interessanter Artikel über die derzeit fehlerhafte Implementierung von „Futures“ in der Standard-C ++ 11-Bibliothek ist hier zu lesen: Broken Promises - C ++ 0x-Futures .
Wie würde eine Call-Site in Objective-C das Ergebnis erzielen?
Nun, es ist wahrscheinlich am besten, ein paar Beispiele zu zeigen. Es gibt einige Bibliotheken, die ein Versprechen implementieren (siehe Links unten).
Für die nächsten Codefragmente verwende ich jedoch eine bestimmte Implementierung einer Promise-Bibliothek, die auf GitHub RXPromise verfügbar ist . Ich bin der Autor von RXPromise.
Die anderen Implementierungen verfügen möglicherweise über eine ähnliche API, es kann jedoch kleine und möglicherweise geringfügige Unterschiede in der Syntax geben. RXPromise ist eine Objective-C-Version der Promise / A + -Spezifikation, die einen offenen Standard für die robuste und interoperable Implementierung von Versprechungen in JavaScript definiert.
Alle unten aufgeführten Versprechungsbibliotheken implementieren den asynchronen Stil.
Es gibt erhebliche Unterschiede zwischen den verschiedenen Implementierungen. RXPromise verwendet intern die Versandbibliothek, ist vollständig threadsicher, extrem leicht und bietet eine Reihe zusätzlicher nützlicher Funktionen, wie zum Beispiel die Stornierung.
Eine Call-Site erhält das endgültige Ergebnis der asynchronen Aufgabe durch "Registrieren" von Handlern. Die „Promise / A + -Spezifikation“ definiert die Methode
then
.Die Methode
then
Mit RXPromise sieht es folgendermaßen aus:
wobei successHandler ein Block ist, der aufgerufen wird, wenn die Zusage "erfüllt" wurde, und errorHandler ein Block ist, der aufgerufen wird, wenn die Zusage "abgelehnt" wurde.
In RXPromise haben die Handlerblöcke die folgende Signatur:
Der success_handler hat einen Parameter Ergebnis , die offensichtlich das schließliche Ergebnis der asynchronen Aufgabe. Ebenso weist der error_handler einen Parameterfehler auf , der der Fehler ist, der von der asynchronen Task gemeldet wurde, als dieser fehlgeschlagen ist.
Beide Blöcke haben einen Rückgabewert. Worum es bei diesem Rückgabewert geht, wird bald klar.
In RXPromise
then
ist dies eine Eigenschaft, die einen Block zurückgibt. Dieser Block hat zwei Parameter, den Success-Handler-Block und den Error-Handler-Block. Die Handler müssen von der Call-Site definiert werden.Der Ausdruck
promise.then(success_handler, error_handler);
ist also eine Kurzform vonWir können noch präziseren Code schreiben:
Der Code lautet: "DoSomethingAsync ausführen , wenn es erfolgreich ist, dann Erfolgshandler ausführen".
Hier ist der Fehlerbehandler,
nil
was bedeutet, dass er im Fehlerfall in diesem Versprechen nicht behandelt wird.Eine weitere wichtige Tatsache ist, dass der Aufruf des von property zurückgegebenen Blocks
then
ein Promise zurückgibt:Beim Aufruf des von property zurückgegebenen Blocks
then
gibt der „Empfänger“ ein neues Versprechen zurück, ein untergeordnetes Versprechen. Der Empfänger wird zum übergeordneten Versprechen.Was bedeutet das?
Nun, aufgrund dessen können wir asynchrone Aufgaben "verketten", die effektiv sequentiell ausgeführt werden.
Darüber hinaus wird der Rückgabewert eines der Handler zum „Wert“ des zurückgegebenen Versprechens. Wenn die Aufgabe mit dem Ergebnis "OK" erfolgreich ist, wird das zurückgegebene Versprechen mit dem Wert "OK" "gelöst" (dh "erfüllt"):
Wenn die asynchrone Aufgabe fehlschlägt, wird das zurückgegebene Versprechen ebenfalls mit einem Fehler aufgelöst (dh "abgelehnt").
Der Handler kann auch ein anderes Versprechen zurückgeben. Zum Beispiel, wenn dieser Handler eine andere asynchrone Aufgabe ausführt. Mit diesem Mechanismus können wir asynchrone Aufgaben „verketten“:
Wenn es kein untergeordnetes Versprechen gibt, hat der Rückgabewert keine Auswirkung.
Ein komplexeres Beispiel:
Hier führen wir
asyncTaskA
,asyncTaskB
,asyncTaskC
und derasyncTaskD
Reihe nach - und jede weitere Aufgabe übernimmt das Ergebnis der vorhergehenden Aufgabe als Eingabe:Eine solche "Kette" wird auch "Fortsetzung" genannt.
Fehlerbehandlung
Versprechen machen den Umgang mit Fehlern besonders einfach. Fehler werden vom übergeordneten Element an das untergeordnete Element weitergeleitet, wenn im übergeordneten Versprechen kein Fehlerbehandlungsprogramm definiert ist. Der Fehler wird in der Kette weitergeleitet, bis ein Kind ihn bearbeitet. Mit der obigen Kette können wir also die Fehlerbehandlung implementieren, indem wir eine weitere „Fortsetzung“ hinzufügen, die sich mit einem potenziellen Fehler befasst, der irgendwo darüber auftreten kann :
Dies entspricht dem wahrscheinlich bekannteren synchronen Stil mit Ausnahmebehandlung:
Versprechen im Allgemeinen haben andere nützliche Funktionen:
Wenn Sie beispielsweise einen Verweis auf ein Versprechen haben, können Sie über
then
"registrieren" so viele Handler wie gewünscht. In RXPromise können Handler jederzeit und von jedem Thread aus registriert werden, da sie vollständig threadsicher sind.RXPromise bietet einige weitere nützliche Funktionen, die von der Promise / A + -Spezifikation nicht benötigt werden. Einer ist "Stornierung".
Es stellte sich heraus, dass "Stornierung" ein unschätzbares und wichtiges Merkmal ist. Zum Beispiel kann eine Call-Site, die einen Verweis auf ein Versprechen enthält, die
cancel
Nachricht an sie senden, um anzuzeigen, dass sie nicht mehr am endgültigen Ergebnis interessiert ist.Stellen Sie sich eine asynchrone Aufgabe vor, die ein Bild aus dem Web lädt und in einem View-Controller angezeigt werden soll. Wenn sich der Benutzer vom aktuellen Ansichtscontroller entfernt, kann der Entwickler Code implementieren, der eine Abbruchnachricht an imagePromise sendet , die wiederum den durch die HTTP-Anforderungsoperation definierten Fehlerhandler auslöst, bei dem die Anforderung abgebrochen wird.
In RXPromise wird eine Abbruchnachricht nur von einem übergeordneten Element an seine untergeordneten Elemente weitergeleitet, nicht jedoch umgekehrt. Das heißt, ein "Wurzel" -Versprechen hebt alle Kinder-Versprechen auf. Ein Versprechen eines Kindes wird jedoch nur den Zweig stornieren, in dem es sich um den Elternteil handelt. Die Abbruchnachricht wird auch an Kinder weitergeleitet, wenn ein Versprechen bereits gelöst wurde.
Eine asynchrone Task kann selbst einen Handler für ihr eigenes Versprechen registrieren und somit erkennen, wann jemand anderes sie storniert hat. Es kann dann vorzeitig aufhören, eine möglicherweise langwierige und kostspielige Aufgabe auszuführen.
Hier sind einige andere Implementierungen von Promises in Objective-C, die auf GitHub zu finden sind:
https://github.com/Schoonology/aplus-objc
https://github.com/affablebloke/deferred-objective-c
https://github.com/bww/FutureKit
https://github.com/jkubicek/JKPromises
https://github.com/Strilanc/ObjC-CollapsingFutures
https://github.com/b52/OMPromises
https://github.com/mproberts/objc-promise
https://github.com/klaaspieter/Promise
https: //github.com/jameswomack/Promise
https://github.com/nilfs/promise-objc
https://github.com/mxcl/PromiseKit
https://github.com/apleshkov/promises-aplus
https: // github.com/KptainO/Rebelle
und meine eigene Implementierung: RXPromise .
Diese Liste ist wahrscheinlich nicht vollständig!
Überprüfen Sie bei der Auswahl einer dritten Bibliothek für Ihr Projekt sorgfältig, ob die Implementierung der Bibliothek die folgenden Voraussetzungen erfüllt:
Eine zuverlässige Versprechungsbibliothek MUSS threadsicher sein!
Es geht nur um asynchrone Verarbeitung, und wir möchten, wann immer möglich, mehrere CPUs verwenden und auf verschiedenen Threads gleichzeitig ausführen. Seien Sie vorsichtig, die meisten Implementierungen sind nicht threadsicher!
Handler MÜSSEN asynchron aufgerufen werden, was die Aufrufstelle betrifft ! Immer und egal was!
Jede anständige Implementierung sollte auch beim Aufrufen der asynchronen Funktionen einem sehr strengen Muster folgen. Viele Implementierer tendieren dazu, den Fall zu "optimieren", in dem ein Handler synchron aufgerufen wird, wenn das Versprechen bereits gelöst ist, wenn der Handler registriert wird. Dies kann zu allen möglichen Problemen führen. Siehe Zalgo nicht freigeben! .
Es sollte auch einen Mechanismus geben, um ein Versprechen zu stornieren.
Die Möglichkeit, eine asynchrone Aufgabe abzubrechen, wird häufig zu einer Anforderung mit hoher Priorität in der Anforderungsanalyse. Andernfalls wird sicher einige Zeit später nach der Freigabe der App eine Erweiterungsanfrage von einem Benutzer gestellt. Der Grund sollte offensichtlich sein: Jede Aufgabe, die zum Stillstand kommt oder zu lange dauert, sollte vom Benutzer oder durch eine Zeitüberschreitung abgebrochen werden können. Eine anständige Versprechensbibliothek sollte die Stornierung unterstützen.
quelle
Mir ist klar, dass dies eine alte Frage ist, aber ich muss sie beantworten, weil meine Antwort anders ist als die der anderen.
Für diejenigen, die sagen, es ist eine Frage der persönlichen Präferenz, muss ich nicht zustimmen. Es gibt einen guten, logischen Grund, den einen dem anderen vorzuziehen ...
Im Abschlussfall werden Ihrem Block zwei Objekte ausgehändigt, eines steht für Erfolg, während das andere für Misserfolg steht. Was machen Sie also, wenn beide Null sind? Was machst du, wenn beide einen Wert haben? Dies sind Fragen, die bei der Kompilierung vermieden werden können und sollten. Sie vermeiden diese Fragen, indem Sie zwei separate Blöcke haben.
Durch separate Erfolgs- und Fehlerblöcke ist Ihr Code statisch überprüfbar.
Beachten Sie, dass sich die Dinge mit Swift ändern. Darin können wir den Begriff einer
Either
Aufzählung so implementieren , dass der einzelne Vervollständigungsblock garantiert entweder ein Objekt oder einen Fehler aufweist und genau einen von diesen haben muss. Für Swift ist also ein einzelner Block besser.quelle
Ich vermute, es wird eine persönliche Präferenz sein ...
Ich bevorzuge aber die getrennten Erfolgs- / Misserfolgsblöcke. Ich mag es, die Erfolgs- / Misserfolgslogik zu trennen. Wenn Sie Erfolg / Misserfolg verschachtelt hätten, hätten Sie am Ende etwas, das besser lesbar wäre (zumindest meiner Meinung nach).
Als ein relativ extremes Beispiel für eine solche Verschachtelung sehen Sie hier einen Rubin , der dieses Muster zeigt.
quelle
Das fühlt sich an wie eine vollständige Überarbeitung, aber ich glaube nicht, dass es hier eine richtige Antwort gibt. Ich habe mich für den Abschlussblock entschieden, nur weil die Fehlerbehandlung möglicherweise noch im Erfolgszustand durchgeführt werden muss, wenn Erfolgs- / Fehlerblöcke verwendet werden.
Ich denke, der endgültige Code wird ungefähr so aussehen
oder einfach
Nicht das beste Stück Code und das Verschachteln wird noch schlimmer
Ich denke, ich werde für eine Weile mope.
quelle