Je dirais que si l'API fournit un gestionnaire d'achèvement ou une paire de blocs de réussite / échec, c'est principalement une question de préférence personnelle.
Les deux approches ont des avantages et des inconvénients, bien qu'il n'y ait que des différences marginales.
Songez qu'il ya aussi d' autres variantes, par exemple lorsque l' un gestionnaire d'achèvement ne peut avoir qu'un un paramètre combinant le résultat final ou une erreur potentielle:
typedef void (^completion_t)(id result);
- (void) taskWithCompletion:(completion_t)completionHandler;
[self taskWithCompletion:^(id result){
if ([result isKindOfError:[NSError class]) {
NSLog(@"Error: %@", result);
}
else {
...
}
}];
Le but de cette signature est qu'un gestionnaire d'achèvement peut être utilisé de manière générique dans d'autres API.
Par exemple, dans Catégorie pour NSArray, il existe une méthode forEachApplyTask:completion:
qui appelle séquentiellement une tâche pour chaque objet et rompt la boucle IFF en cas d'erreur. Comme cette méthode est elle-même asynchrone, elle possède également un gestionnaire de complétion:
typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);
En fait, completion_t
tel que défini ci-dessus est suffisamment générique et suffisant pour gérer tous les scénarios.
Cependant, il existe d'autres moyens pour une tâche asynchrone de signaler sa notification d'achèvement au site d'appel:
Promesses
Les promesses, aussi appelé « à terme », « différés » ou « retardée » représentent l' éventuel résultat d'une tâche asynchrone (voir aussi: wiki Futures et promesses ).
Initialement, une promesse est dans l'état «en attente». Autrement dit, sa «valeur» n'est pas encore évaluée et n'est pas encore disponible.
Dans Objective-C, une promesse serait un objet ordinaire qui sera renvoyé d'une méthode asynchrone comme indiqué ci-dessous:
- (Promise*) doSomethingAsync;
! L'état initial d'une promesse est «en attente».
Pendant ce temps, les tâches asynchrones commencent à évaluer son résultat.
Notez également qu'il n'y a pas de gestionnaire d'achèvement. Au lieu de cela, la Promesse fournira un moyen plus puissant où le site d'appel peut obtenir le résultat final de la tâche asynchrone, que nous verrons bientôt.
La tâche asynchrone, qui a créé l'objet de promesse, DOIT finalement «résoudre» sa promesse. Cela signifie qu'une tâche pouvant réussir ou échouer, elle DOIT soit «tenir» une promesse en lui transmettant le résultat évalué, soit elle doit «rejeter» la promesse en lui passant une erreur indiquant la raison de l'échec.
! Une tâche doit finalement tenir sa promesse.
Lorsqu'une promesse a été résolue, elle ne peut plus changer son état, y compris sa valeur.
! Une promesse ne peut être résolue qu'une seule fois .
Une fois qu'une promesse a été résolue, un site d'appel peut obtenir le résultat (qu'il ait échoué ou réussi). La manière dont cela est accompli dépend de l'implémentation de la promesse à l'aide du style synchrone ou asynchrone.
A Promise peut être mis en oeuvre dans un mode synchrone ou asynchrone un qui conduit soit à bloquer , respectivement non-blocage sémantique.
Dans un style synchrone afin de récupérer la valeur de la promesse, un site d'appel utiliserait une méthode qui bloquera le thread actuel jusqu'à ce que la promesse ait été résolue par la tâche asynchrone et que le résultat final soit disponible.
Dans un style asynchrone, le site d'appel enregistre les rappels ou les blocs de gestionnaire qui sont appelés immédiatement après la résolution de la promesse.
Il s'est avéré que le style synchrone présente un certain nombre d'inconvénients importants qui déjouent efficacement les mérites des tâches asynchrones. Un article intéressant sur l'implémentation actuellement imparfaite de «futures» dans la bibliothèque C ++ 11 standard peut être lu ici: Broken promises – C ++ 0x futures .
Comment, dans Objective-C, un site d'appel obtiendrait-il le résultat?
Eh bien, il vaut probablement mieux montrer quelques exemples. Il existe quelques bibliothèques qui implémentent une promesse (voir les liens ci-dessous).
Cependant, pour les prochains extraits de code, j'utiliserai une implémentation particulière d'une bibliothèque Promise, disponible sur GitHub RXPromise . Je suis l'auteur de RXPromise.
Les autres implémentations peuvent avoir une API similaire, mais il peut y avoir de petites et éventuellement subtiles différences de syntaxe. RXPromise est une version Objective-C de la spécification Promise / A + qui définit un standard ouvert pour des implémentations robustes et interopérables de promesses en JavaScript.
Toutes les bibliothèques de promesses répertoriées ci-dessous implémentent le style asynchrone.
Il existe des différences assez importantes entre les différentes implémentations. RXPromise utilise en interne la bibliothèque de répartition, est entièrement sûr pour les threads, extrêmement léger et fournit également un certain nombre de fonctionnalités utiles supplémentaires, telles que l'annulation.
Un site d'appel obtient le résultat final de la tâche asynchrone par le biais de gestionnaires «d'enregistrement». La «spécification Promise / A +» définit la méthode then
.
La méthode then
Avec RXPromise, cela ressemble à ceci:
promise.then(successHandler, errorHandler);
où successHandler est un bloc qui est appelé lorsque la promesse a été «remplie» et errorHandler est un bloc qui est appelé lorsque la promesse a été «rejetée».
! then
est utilisé pour obtenir le résultat final et pour définir un gestionnaire de réussite ou d'erreur.
Dans RXPromise, les blocs de gestionnaire ont la signature suivante:
typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
Le success_handler a un résultat de paramètre qui est évidemment le résultat final de la tâche asynchrone. De même, le gestionnaire d' erreur a une erreur de paramètre qui est l'erreur signalée par la tâche asynchrone lorsqu'elle a échoué.
Les deux blocs ont une valeur de retour. La nature de cette valeur de retour deviendra claire bientôt.
Dans RXPromise, then
est une propriété qui renvoie un bloc. Ce bloc a deux paramètres, le bloc gestionnaire de réussite et le bloc gestionnaire d'erreur. Les gestionnaires doivent être définis par le site d'appel.
! Les gestionnaires doivent être définis par le site d'appel.
Ainsi, l'expression promise.then(success_handler, error_handler);
est une forme abrégée de
then_block_t block promise.then;
block(success_handler, error_handler);
Nous pouvons écrire du code encore plus concis:
doSomethingAsync
.then(^id(id result){
…
return @“OK”;
}, nil);
Le code indique: «Exécutez doSomethingAsync, quand il réussit, puis exécutez le gestionnaire de réussite».
Ici, le gestionnaire d'erreur est nil
ce qui signifie qu'en cas d'erreur, il ne sera pas traité dans cette promesse.
Un autre fait important est que l'appel du bloc renvoyé par la propriété then
retournera une promesse:
! then(...)
retourne une promesse
Lors de l'appel du bloc renvoyé par la propriété then
, le «récepteur» renvoie une nouvelle promesse, une promesse enfant . Le récepteur devient la promesse parentale .
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
Qu'est-ce que ça veut dire?
Eh bien, de ce fait, nous pouvons «enchaîner» des tâches asynchrones qui sont exécutées de manière séquentielle.
En outre, la valeur de retour de l'un ou l'autre gestionnaire deviendra la «valeur» de la promesse retournée. Donc, si la tâche réussit avec le résultat final @ «OK», la promesse retournée sera «résolue» (c'est-à-dire «remplie») avec la valeur @ «OK»:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return @"OK";
}, nil);
...
assert([[returnedPromise get] isEqualToString:@"OK"]);
De même, lorsque la tâche asynchrone échoue, la promesse retournée sera résolue (c'est-à-dire «rejetée») avec une erreur.
RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
return error;
});
...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);
Le gestionnaire peut également retourner une autre promesse. Par exemple, lorsque ce gestionnaire exécute une autre tâche asynchrone. Avec ce mécanisme, nous pouvons «chaîner» des tâches asynchrones:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return asyncB(result);
}, nil);
! La valeur de retour d'un bloc de gestionnaire devient la valeur de la promesse enfant.
S'il n'y a pas de promesse enfant, la valeur de retour n'a aucun effet.
Un exemple plus complexe:
Ici, nous exécutons asyncTaskA
, asyncTaskB
, asyncTaskC
et asyncTaskD
successivement - et chaque tâche suivante prend le résultat de la tâche précédente comme entrée:
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);
Une telle «chaîne» est également appelée «continuation».
La gestion des erreurs
Les promesses facilitent particulièrement la gestion des erreurs. Les erreurs seront «transmises» du parent à l'enfant s'il n'y a pas de gestionnaire d'erreurs défini dans la promesse du parent. L'erreur sera transmise vers le haut de la chaîne jusqu'à ce qu'un enfant la gère. Ainsi, ayant la chaîne ci-dessus, nous pouvons implémenter la gestion des erreurs simplement en ajoutant une autre «continuation» qui traite d'une erreur potentielle qui peut se produire n'importe où ci - dessus :
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;
});
Cela s'apparente au style synchrone probablement plus familier avec la gestion des exceptions:
try {
id a = A();
id b = B(a);
id c = C(b);
id d = D(c);
// handle d
}
catch (NSError* error) {
NSLog(@“”Error: %@“, error);
}
Les promesses ont en général d'autres caractéristiques utiles:
Par exemple, ayant une référence à une promesse, via then
on peut "enregistrer" autant de gestionnaires que souhaité. Dans RXPromise, l'enregistrement des gestionnaires peut se produire à tout moment et à partir de n'importe quel thread car il est entièrement thread-safe.
RXPromise a quelques fonctionnalités fonctionnelles plus utiles, non requises par la spécification Promise / A +. L'une est "l'annulation".
Il s'est avéré que «l'annulation» est une caractéristique inestimable et importante. Par exemple, un site d'appel détenant une référence à une promesse peut lui envoyer le cancel
message afin d'indiquer qu'il n'est plus intéressé par le résultat final.
Imaginez simplement une tâche asynchrone qui charge une image à partir du Web et qui doit être affichée dans un contrôleur de vue. Si l'utilisateur s'éloigne du contrôleur de vue actuel, le développeur peut implémenter du code qui envoie un message d'annulation à l' imagePromise , qui à son tour déclenche le gestionnaire d'erreurs défini par l'opération de demande HTTP où la demande sera annulée.
Dans RXPromise, un message d'annulation ne sera transmis que d'un parent à ses enfants, mais pas l'inverse. Autrement dit, une promesse «racine» annulera toutes les promesses d'enfants. Mais une promesse d'enfant n'annulera que la «branche» où se trouve le parent. Le message d'annulation sera également transmis aux enfants si une promesse a déjà été résolue.
Une tâche asynchrone peut elle - même enregistrer le gestionnaire pour sa propre promesse et ainsi détecter quand quelqu'un d'autre l'a annulée. Il peut alors cesser prématurément d'effectuer une tâche éventuellement longue et coûteuse.
Voici quelques autres implémentations de Promises in Objective-C trouvées sur GitHub:
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
et ma propre implémentation: RXPromise .
Cette liste n'est probablement pas complète!
Lorsque vous choisissez une troisième bibliothèque pour votre projet, veuillez vérifier attentivement si la mise en œuvre de la bibliothèque respecte les conditions requises énumérées ci-dessous:
Une bibliothèque de promesses fiable DOIT être sûre pour les threads!
Il s'agit du traitement asynchrone, et nous voulons utiliser plusieurs processeurs et exécuter simultanément sur différents threads dans la mesure du possible. Attention, la plupart des implémentations ne sont pas thread-safe!
Les gestionnaires DOIVENT être appelés de manière asynchrone, en respectant le site d'appel! Toujours et quoi qu'il arrive!
Toute implémentation décente doit également suivre un modèle très strict lors de l'appel des fonctions asynchrones. De nombreux implémenteurs ont tendance à "optimiser" le cas où un gestionnaire sera appelé de manière synchrone lorsque la promesse est déjà résolue lorsque le gestionnaire sera enregistré. Cela peut provoquer toutes sortes de problèmes. Voir Ne pas libérer Zalgo! .
Il devrait également y avoir un mécanisme pour annuler une promesse.
La possibilité d'annuler une tâche asynchrone devient souvent une exigence de haute priorité dans l'analyse des exigences. Sinon, il est certain qu'une demande d'amélioration sera déposée par un utilisateur un peu plus tard après la sortie de l'application. La raison doit être évidente: toute tâche qui peut se bloquer ou prendre trop de temps à terminer, doit être annulable par l'utilisateur ou par un timeout. Une bibliothèque de promesses décentes devrait prendre en charge l'annulation.