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 :
typedef void (^completion_t)(id result);
- (void) taskWithCompletion:(completion_t)completionHandler;
[self taskWithCompletion:^(id result){
if ([result isKindOfError:[NSError class]) {
NSLog(@"Error: %@", result);
}
else {
...
}
}];
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:
typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);
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:
- (Promise*) doSomethingAsync;
! Der Ausgangszustand eines Versprechens ist "ausstehend".
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.
! Eine Aufgabe muss schließlich ihr Versprechen lösen.
Wenn ein Versprechen aufgelöst wurde, kann es seinen Status, einschließlich seines Werts, nicht mehr ändern.
! Ein Versprechen kann nur einmal gelöst werden .
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:
promise.then(successHandler, errorHandler);
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.
! then
wird verwendet, um das endgültige Ergebnis zu erhalten und einen Erfolgs- oder Fehlerhandler zu definieren.
In RXPromise haben die Handlerblöcke die folgende Signatur:
typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
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.
! Die Handler müssen von der Call-Site definiert werden.
Der Ausdruck promise.then(success_handler, error_handler);
ist also eine Kurzform von
then_block_t block promise.then;
block(success_handler, error_handler);
Wir können noch präziseren Code schreiben:
doSomethingAsync
.then(^id(id result){
…
return @“OK”;
}, nil);
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:
! then(...)
gibt ein Versprechen zurück
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.
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
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"):
RXPromise* returnedPromise = asyncA().then(^id(id result){
return @"OK";
}, nil);
...
assert([[returnedPromise get] isEqualToString:@"OK"]);
Wenn die asynchrone Aufgabe fehlschlägt, wird das zurückgegebene Versprechen ebenfalls mit einem Fehler aufgelöst (dh "abgelehnt").
RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
return error;
});
...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);
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“:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return asyncB(result);
}, nil);
! Der Rückgabewert eines Handlerblocks wird zum Wert des untergeordneten Versprechens.
Wenn es kein untergeordnetes Versprechen gibt, hat der Rückgabewert keine Auswirkung.
Ein komplexeres Beispiel:
Hier führen wir asyncTaskA
, asyncTaskB
, asyncTaskC
und der asyncTaskD
Reihe nach - und jede weitere Aufgabe übernimmt das Ergebnis der vorhergehenden Aufgabe als Eingabe:
asyncTaskA()
.then(^id(id result){
return asyncTaskB(result);
}, nil)
.then(^id(id result){
return asyncTaskC(result);
}, nil)
.then(^id(id result){
return asyncTaskD(result);
}, nil)
.then(^id(id result){
// handle result
return nil;
}, nil);
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 :
asyncTaskA()
.then(^id(id result){
return asyncTaskB(result);
}, nil)
.then(^id(id result){
return asyncTaskC(result);
}, nil)
.then(^id(id result){
return asyncTaskD(result);
}, nil)
.then(^id(id result){
// handle result
return nil;
}, nil);
.then(nil, ^id(NSError*error) {
NSLog(@“”Error: %@“, error);
return nil;
});
Dies entspricht dem wahrscheinlich bekannteren synchronen Stil mit Ausnahmebehandlung:
try {
id a = A();
id b = B(a);
id c = C(b);
id d = D(c);
// handle d
}
catch (NSError* error) {
NSLog(@“”Error: %@“, error);
}
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.