Je vois deux modèles courants de blocs dans Objective-C. L'un est une paire de succès:/échec: blocs, l'autre est un seul achèvement: bloc.
Par exemple, disons que j'ai une tâche qui retournera un objet de manière asynchrone et que cette tâche peut échouer. Le premier modèle est -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure
. Le deuxième modèle est -taskWithCompletion:(void (^)(id object, NSError *error))completion
.
[target taskWithSuccess:^(id object) {
// W00t! I've got my object
} failure:^(NSError *error) {
// Oh noes! report the failure.
}];
[target taskWithCompletion:^(id object, NSError *error) {
if (object) {
// W00t! I've got my object
} else {
// Oh noes! report the failure.
}
}];
Quel est le modèle préféré? Quelles sont les forces et les faiblesses? Quand utiliseriez-vous l'un sur l'autre?
Le rappel de fin (opposé à la paire succès/échec) est plus générique. Si vous devez préparer un certain contexte avant de traiter le statut de retour, vous pouvez le faire juste avant la clause "if (object)". En cas de succès/échec, vous devez dupliquer ce code. Cela dépend bien sûr de la sémantique des rappels.
Je dirais que si l'API fournit un gestionnaire d'achèvement ou une paire de blocs de réussite/échec, c'est avant tout une question de personnel préférence.
Les deux approches ont des avantages et des inconvénients, bien qu'il n'y ait que des différences marginales.
Considérez qu'il existe également d'autres variantes, par exemple où le gestionnaire de complétion one peut avoir seulement 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 de complétion peut être utilisé de manière générique dans d'autres API.
Par exemple, dans Category for 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:
Les promesses, également appelées "Futures", "Reportées" ou "Retardées" représentent le résultat éventuel 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.
Une promesse peut être implémentée dans un style synchrone ou asynchrone qui conduit à blocage respectivement sémantique non bloquante .
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 l'asynchrone tâche et le résultat final est 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: promesses brisées - futures C++ 0x .
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
.
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 le la promesse a été "rejetée".
!
then
est utilisé pour obtenir le résultat final et pour définir un succès ou un gestionnaire 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 paramètre result qui est évidemment le résultat final de la tâche asynchrone. De même, le gestionnaire d'erreurs 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écuter doSomethingAsync, quand il réussit, puis exécuter 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
renverra une promesse:
!
then(...)
renvoie une promesse
Lors de l'appel du bloc renvoyé par la propriété then
, le "récepteur" retourne une nouvelle promesse , une promesse enfant . Le récepteur devient la promesse parent .
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
Qu'est-ce que ça veut dire?
Eh bien, pour cette raison, 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 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
séquentiellement - et chaque tâche suivante prend le résultat de la tâche précédente en 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".
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ù au-dessus de :
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 message cancel
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 au 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 peut 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 préalables é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 qui respectent 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 invoqué 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 libérez pas 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.
Je me rends compte que c'est une vieille question mais je dois y répondre parce que ma réponse est différente des autres.
Pour ceux qui disent que c'est une question de préférence personnelle, je dois être en désaccord. Il y a une bonne raison logique de préférer l'un à l'autre ...
Dans le cas de l'achèvement, votre bloc se voit remettre deux objets, l'un représente le succès tandis que l'autre représente l'échec ... Alors, que faites-vous si les deux sont nuls? Que faites-vous si les deux ont une valeur? Ce sont des questions qui peuvent être évitées au moment de la compilation et en tant que telles, elles devraient l'être. Vous évitez ces questions en ayant deux blocs distincts.
Le fait d'avoir des blocs de réussite et d'échec séparés rend votre code statiquement vérifiable.
Notez que les choses changent avec Swift. Dans ce document, nous pouvons implémenter la notion d'un Either
enum de sorte que le bloc d'achèvement unique soit garanti d'avoir un objet ou une erreur, et doit en avoir exactement un. Donc, pour Swift, un seul bloc est préférable.
Je pense que ça va finir par être une préférence personnelle ...
Mais je préfère les blocs séparés succès/échec. J'aime séparer la logique de réussite/échec. Si vous aviez des succès/échecs imbriqués, vous vous retrouveriez avec quelque chose qui serait plus lisible (à mon avis du moins).
Comme exemple relativement extrême d'une telle imbrication, voici quelques rubis montrant ce modèle.
Cela ressemble à un copout complet, mais je ne pense pas qu'il y ait une bonne réponse ici. Je suis allé avec le bloc d'achèvement simplement parce que la gestion des erreurs peut encore devoir être effectuée dans la condition de réussite lors de l'utilisation des blocs de réussite/échec.
Je pense que le code final ressemblera à quelque chose
[target taskWithCompletion:^(id object, NSError *error) {
if (error) {
// Oh noes! report the failure.
} else if (![target validateObject:&object error:&error]) {
// Oh noes! report the failure.
} else {
// W00t! I've got my object
}
}];
ou simplement
[target taskWithCompletion:^(id object, NSError *error) {
if (error || ![target validateObject:&object error:&error]) {
// Oh noes! report the failure.
return;
}
// W00t! I've got my object
}];
Pas le meilleur morceau de code et l'imbrication empire
[target taskWithCompletion:^(id object, NSError *error) {
if (error || ![target validateObject:&object error:&error]) {
// Oh noes! report the failure.
return;
}
[object objectTaskWithCompletion:^(id object2, NSError *error) {
if (error || ![object validateObject2:&object2 error:&error]) {
// Oh noes! report the failure.
return;
}
// W00t! I've got object and object 2
}];
}];
Je pense que je vais me morfondre un moment.