Direi che l'API fornisce un gestore di completamento o una coppia di blocchi di successo / fallimento, è principalmente una questione di preferenze personali.
Entrambi gli approcci hanno pro e contro, anche se ci sono solo differenze marginali.
Considera che ci sono anche altre varianti, ad esempio in cui un gestore di completamento può avere un solo parametro che combina il risultato finale o un potenziale errore:
typedef void (^completion_t)(id result);
- (void) taskWithCompletion:(completion_t)completionHandler;
[self taskWithCompletion:^(id result){
if ([result isKindOfError:[NSError class]) {
NSLog(@"Error: %@", result);
}
else {
...
}
}];
Lo scopo di questa firma è che un gestore di completamento può essere utilizzato genericamente in altre API.
Ad esempio, in Category for NSArray esiste un metodo forEachApplyTask:completion:
che richiama in sequenza un'attività per ciascun oggetto e interrompe il loop IFF. Si è verificato un errore. Poiché questo metodo è anch'esso asincrono, ha anche un gestore di completamento:
typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);
In effetti, completion_t
come definito sopra è abbastanza generico e sufficiente per gestire tutti gli scenari.
Tuttavia, esistono altri mezzi per un'attività asincrona per segnalare la sua notifica di completamento al sito di chiamata:
promesse
Promesse, chiamati anche “Futures”, “in differita” o “ritardato” rappresentano l' eventuale risultato di un'attività asincrona (vedi anche: wiki Futures e promesse ).
Inizialmente, una promessa è nello stato "in sospeso". Cioè, il suo "valore" non è ancora stato valutato e non è ancora disponibile.
In Objective-C, una Promessa sarebbe un oggetto ordinario che verrà restituito da un metodo asincrono come mostrato di seguito:
- (Promise*) doSomethingAsync;
! Lo stato iniziale di una Promessa è "in sospeso".
Nel frattempo, le attività asincrone inizia a valutare il suo risultato.
Si noti inoltre che non esiste un gestore di completamento. Invece, la Promessa fornirà un mezzo più potente in cui il sito di chiamata può ottenere l'eventuale risultato dell'attività asincrona, che vedremo presto.
Il compito asincrono, che ha creato l'oggetto promessa, DEVE infine "risolvere" la sua promessa. Ciò significa che, poiché un'attività può avere esito positivo o negativo, DEVE "adempiere" a una promessa trasmettendole il risultato valutato, oppure DEVE "rifiutare" la promessa trasmettendole un errore che indica la ragione del fallimento.
! Un compito deve infine risolvere la sua promessa.
Quando una Promessa è stata risolta, non può più cambiare il suo stato, incluso il suo valore.
! Una promessa può essere risolta una sola volta .
Una volta risolta una promessa, un sito di chiamata può ottenere il risultato (sia esso fallito o riuscito). Il modo in cui ciò viene realizzato dipende dall'implementazione della promessa mediante lo stile sincrono o asincrono.
Una Promessa può essere implementata in uno stile sincrono o asincrono che porta a bloccare rispettivamente la semantica non bloccante .
In uno stile sincrono per recuperare il valore della promessa, un sito di chiamata utilizzerà un metodo che bloccherà il thread corrente fino a quando la promessa non sarà stata risolta dall'attività asincrona e il risultato finale sarà disponibile.
In uno stile asincrono, il sito di chiamata registrava callback o blocchi di gestori che vengono chiamati immediatamente dopo che la promessa è stata risolta.
Si è scoperto che lo stile sincrono presenta una serie di svantaggi significativi che sconfiggono efficacemente i meriti delle attività asincrone. Un articolo interessante sull'implementazione attualmente errata di "futures" nella libreria standard C ++ 11 può essere letto qui: promesse non mantenute - C ++ 0x futures .
Come, in Objective-C, un sito di chiamata otterrebbe il risultato?
Bene, probabilmente è meglio mostrare alcuni esempi. Ci sono un paio di librerie che implementano una Promessa (vedi link sotto).
Tuttavia, per i prossimi frammenti di codice, userò una particolare implementazione di una libreria Promise, disponibile su GitHub RXPromise . Sono l'autore di RXPromise.
Le altre implementazioni possono avere un'API simile, ma possono esserci differenze piccole e forse sottili nella sintassi. RXPromise è una versione Objective-C della specifica Promise / A + che definisce uno standard aperto per implementazioni solide e interoperabili di promesse in JavaScript.
Tutte le librerie promesse elencate di seguito implementano lo stile asincrono.
Ci sono differenze abbastanza significative tra le diverse implementazioni. RXPromise utilizza internamente la libreria di invio, è completamente thread-safe, estremamente leggera e offre anche una serie di funzioni utili aggiuntive, come la cancellazione.
Un sito di chiamata ottiene il risultato finale dell'attività asincrona tramite i gestori di "registrazione". La "specifica Promise / A +" definisce il metodo then
.
Il metodo then
Con RXPromise appare come segue:
promise.then(successHandler, errorHandler);
dove successHandler è un blocco che viene chiamato quando la promessa è stata “adempiuta” ed errorHandler è un blocco che viene chiamato quando la promessa è stata “rifiutata”.
! then
viene utilizzato per ottenere il risultato finale e per definire un gestore di errori o di successo.
In RXPromise, i blocchi del gestore hanno la seguente firma:
typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
Success_handler ha un risultato di parametro che è ovviamente il risultato finale dell'attività asincrona. Allo stesso modo, error_handler ha un errore di parametro che è l'errore segnalato dall'attività asincrona in caso di errore.
Entrambi i blocchi hanno un valore di ritorno. Di cosa tratta questo valore di ritorno, diventerà presto chiaro.
In RXPromise, then
è una proprietà che restituisce un blocco. Questo blocco ha due parametri, il blocco gestore di successo e il blocco gestore errori. I gestori devono essere definiti dal sito di chiamata.
! I gestori devono essere definiti dal sito di chiamata.
Quindi, l'espressione promise.then(success_handler, error_handler);
è una forma breve di
then_block_t block promise.then;
block(success_handler, error_handler);
Possiamo scrivere un codice ancora più conciso:
doSomethingAsync
.then(^id(id result){
…
return @“OK”;
}, nil);
Il codice dice: "Esegui doSomethingAsync, quando ha successo, quindi esegui il gestore dei successi".
Qui, il gestore degli errori è il nil
che significa che, in caso di errore, non verrà gestito in questa promessa.
Un altro fatto importante è che chiamare il blocco restituito dalla proprietà then
restituirà una promessa:
! then(...)
restituisce una promessa
Quando si chiama il blocco restituito dalla proprietà then
, il "destinatario" restituisce una nuova Promessa, una promessa figlio . Il destinatario diventa la promessa del genitore .
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
Cosa significa?
Bene, a causa di ciò possiamo "concatenare" attività asincrone che vengono effettivamente eseguite in sequenza.
Inoltre, il valore di ritorno di entrambi i gestori diventerà il "valore" della promessa restituita. Pertanto, se l'attività ha esito positivo con "OK", la promessa restituita sarà "risolta" (ovvero "adempiuta") con valore @ "OK":
RXPromise* returnedPromise = asyncA().then(^id(id result){
return @"OK";
}, nil);
...
assert([[returnedPromise get] isEqualToString:@"OK"]);
Allo stesso modo, quando l'attività asincrona fallisce, la promessa restituita verrà risolta (ovvero "rifiutata") con un errore.
RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
return error;
});
...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);
Il gestore può anche restituire un'altra promessa. Ad esempio quando quel gestore esegue un'altra attività asincrona. Con questo meccanismo possiamo "concatenare" attività asincrone:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return asyncB(result);
}, nil);
! Il valore restituito di un blocco gestore diventa il valore della promessa figlio.
Se non vi è alcuna promessa figlio, il valore restituito non ha alcun effetto.
Un esempio più complesso:
Qui, eseguiamo asyncTaskA
, asyncTaskB
, asyncTaskC
e asyncTaskD
sequenzialmente - ed ogni successiva operazione prende il risultato del compito precedente come input:
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);
Tale "catena" è anche chiamata "continuazione".
Gestione degli errori
Le promesse rendono particolarmente facile la gestione degli errori. Gli errori verranno "inoltrati" dal genitore al figlio se non è stato definito un gestore errori nella promessa del genitore. L'errore verrà inoltrato nella catena fino a quando non viene gestito da un bambino. Pertanto, avendo la catena di cui sopra, possiamo implementare la gestione degli errori semplicemente aggiungendo un'altra "continuazione" che si occupa di un potenziale errore che può accadere ovunque sopra :
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;
});
Questo è simile allo stile sincrono probabilmente più familiare con gestione delle eccezioni:
try {
id a = A();
id b = B(a);
id c = C(b);
id d = D(c);
// handle d
}
catch (NSError* error) {
NSLog(@“”Error: %@“, error);
}
Le promesse in generale hanno altre utili funzionalità:
Ad esempio, avendo un riferimento a una promessa, tramite then
uno si può "registrare" tutti i gestori desiderati. In RXPromise, la registrazione dei gestori può avvenire in qualsiasi momento e da qualsiasi thread poiché è completamente thread-safe.
RXPromise ha un paio di funzionalità funzionali più utili, non richieste dalla specifica Promise / A +. Uno è "cancellazione".
Si è scoperto che la "cancellazione" è una caratteristica inestimabile e importante. Ad esempio, un sito di chiamata in possesso di un riferimento a una promessa può inviargli il cancel
messaggio per indicare che non è più interessato al risultato finale.
Immagina semplicemente un'attività asincrona che carica un'immagine dal Web e che deve essere visualizzata in un controller di visualizzazione. Se l'utente si allontana dal controller di vista corrente, lo sviluppatore può implementare il codice che invia un messaggio di annullamento a imagePromise , che a sua volta attiva il gestore degli errori definito dall'operazione di richiesta HTTP in cui la richiesta verrà annullata.
In RXPromise, un messaggio di annullamento verrà inoltrato solo da un genitore ai suoi figli, ma non viceversa. Cioè, una promessa "radice" annullerà tutte le promesse dei bambini. Ma la promessa di un bambino annullerà il "ramo" solo dove è il genitore. Il messaggio di annullamento verrà inoltre inoltrato ai bambini se una promessa è già stata risolta.
Un'attività asincrona può essa stessa registrare il gestore per la propria promessa e quindi rilevare quando qualcun altro l'ha annullata. Potrebbe quindi interrompere prematuramente l'esecuzione di un'attività possibilmente lunga e costosa.
Ecco un paio di altre implementazioni di Promises in Objective-C trovate su 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
e la mia implementazione: RXPromise .
Questo elenco probabilmente non è completo!
Quando scegli una terza libreria per il tuo progetto, controlla attentamente se l'implementazione della libreria segue i prerequisiti elencati di seguito:
Una libreria promessa affidabile DEVE essere sicura per i thread!
Si tratta dell'elaborazione asincrona e vogliamo utilizzare più CPU ed eseguire contemporaneamente su thread diversi quando possibile. Fai attenzione, la maggior parte delle implementazioni non sono thread-safe!
I gestori DEVONO essere chiamati in modo asincrono rispetto al sito di chiamata! Sempre e non importa cosa!
Qualsiasi implementazione decente dovrebbe anche seguire uno schema molto rigido quando si invocano le funzioni asincrone. Molti implementatori tendono a "ottimizzare" il caso in cui un gestore verrà invocato in modo sincrono quando la promessa è già risolta quando il gestore verrà registrato. Ciò può causare tutti i tipi di problemi. Vedi Non rilasciare Zalgo! .
Dovrebbe esserci anche un meccanismo per annullare una promessa.
La possibilità di annullare un'attività asincrona spesso diventa un requisito con priorità elevata nell'analisi dei requisiti. In caso contrario, verrà sicuramente inviata una richiesta di miglioramento da parte di un utente qualche tempo dopo il rilascio dell'app. Il motivo dovrebbe essere ovvio: qualsiasi attività che potrebbe interrompersi o richiedere troppo tempo per essere completata, dovrebbe essere annullata dall'utente o da un timeout. Una biblioteca promettente decente dovrebbe supportare la cancellazione.