successo: / fallimento: blocchi vs completamento: blocco


23

Vedo due schemi comuni per i blocchi in Objective-C. Uno è una coppia di successo: / fallimento: blocchi, l'altro è un singolo completamento: blocco.

Ad esempio, supponiamo che ho un'attività che restituirà un oggetto in modo asincrono e che l'attività potrebbe non riuscire. Il primo modello è -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure. Il secondo modello è -taskWithCompletion:(void (^)(id object, NSError *error))completion.

successo: il fallimento /:

[target taskWithSuccess:^(id object) {
    // W00t! I've got my object
} failure:^(NSError *error) {
    // Oh noes! report the failure.
}];

completamento:

[target taskWithCompletion:^(id object, NSError *error) {
    if (object) {
        // W00t! I've got my object
    } else {
        // Oh noes! report the failure.
    }
}];

Qual è il modello preferito? Quali sono i punti di forza e di debolezza? Quando useresti l'uno sopra l'altro?


Sono abbastanza sicuro che Objective-C abbia una gestione delle eccezioni con lancio / cattura, c'è un motivo per cui non puoi usarlo?
FrustratedWithFormsDesigner,

Ognuno di questi permette di concatenare chiamate asincrone, che le eccezioni non ti danno.
Frank Shearar,

5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - objc idiomatica non usa try / catch per il controllo del flusso.
Ant

1
Ti preghiamo di considerare di spostare la tua risposta dalla domanda a una risposta ... dopo tutto, è una risposta (e puoi rispondere alle tue domande).

1
Alla fine ho ceduto alla pressione dei pari e ho spostato la mia risposta su una risposta effettiva.
Jeffery Thomas,

Risposte:


8

Il callback di completamento (al contrario della coppia successo / fallimento) è più generico. Se è necessario preparare un contesto prima di gestire lo stato di restituzione, è possibile farlo immediatamente prima della clausola "if (oggetto)". In caso di successo / fallimento devi duplicare questo codice. Ciò dipende ovviamente dalla semantica della richiamata.


Non posso commentare la domanda originale ... Le eccezioni non sono valide per il controllo del flusso nell'obiettivo-c (bene, il cacao) e non dovrebbero essere usate come tali. L'eccezione generata deve essere rilevata solo per terminare con grazia.

Sì, posso vederlo. Se è -task…possibile restituire l'oggetto, ma l'oggetto non si trova nello stato corretto, sarà comunque necessario gestire gli errori in condizioni di successo.
Jeffery Thomas,

Sì, e se il blocco non è sul posto, ma viene passato come argomento al controller, devi lanciare due blocchi in giro. Questo può essere noioso quando il callback deve essere passato attraverso molti livelli. Tuttavia, puoi sempre dividerlo / ricomporlo.

Non capisco come il gestore di completamento sia più generico. Il completamento fondamentalmente trasforma più parametri di metodo in uno, sotto forma di parametri di blocco. Inoltre, generico significa meglio? In MVC spesso hai anche un codice duplicato nel controller di visualizzazione, questo è un male necessario a causa della separazione delle preoccupazioni. Non penso che sia un motivo per stare lontano da MVC.
Boon,

@Boon Uno dei motivi per cui vedo il gestore singolo come più generico è per i casi in cui si preferisce che il call / handler / block stesso determini se un'operazione ha avuto esito positivo o negativo. Prendi in considerazione casi di successo parziali in cui potresti possedere un oggetto con dati parziali e il tuo oggetto errore è un errore che indica che non tutti i dati sono stati restituiti. Il blocco potrebbe esaminare i dati stessi e verificare se è sufficiente. Ciò non è possibile con lo scenario di callback binario success / fail.
Travis,

8

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_tcome 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”.

! thenviene 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 nilche significa che, in caso di errore, non verrà gestito in questa promessa.

Un altro fatto importante è che chiamare il blocco restituito dalla proprietà thenrestituirà 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, asyncTaskCe 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 thenuno 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 cancelmessaggio 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.


1
Questo ottiene il premio per la non risposta più lunga di sempre. Ma A per sforzo :-)
Traveling Man

3

Mi rendo conto che questa è una vecchia domanda, ma devo rispondere perché la mia risposta è diversa dalle altre.

Per quelli che dicono che è una questione di preferenze personali, non sono d'accordo. C'è una buona ragione logica per preferire l'una all'altra ...

Nel caso del completamento, al tuo blocco vengono consegnati due oggetti, uno rappresenta il successo mentre l'altro rappresenta il fallimento ... Quindi cosa fai se entrambi sono nulli? Cosa fai se entrambi hanno un valore? Queste sono domande che possono essere evitate in fase di compilazione e come tali dovrebbero essere. Puoi evitare queste domande avendo due blocchi separati.

Avere blocchi di successo e fallimento separati rende staticamente verificabile il tuo codice.


Nota che le cose cambiano con Swift. In esso, possiamo implementare la nozione di Eitherenum in modo tale che il singolo blocco di completamento abbia un oggetto o un errore e ne debba avere esattamente uno. Quindi per Swift è meglio un singolo blocco.


1

Ho il sospetto che finirà per essere una preferenza personale ...

Ma preferisco i blocchi separati successo / fallimento. Mi piace separare la logica di successo / fallimento. Se avessi successo / insuccesso annidato, finiresti con qualcosa che sarebbe più leggibile (almeno secondo me).

Come esempio relativamente estremo di tale nidificazione, ecco alcuni rubini che mostrano questo schema.


1
Ho visto catene nidificate di entrambi. Penso che entrambi abbiano un aspetto terribile, ma questa è la mia opinione personale.
Jeffery Thomas,

1
In quale altro modo è possibile concatenare chiamate asincrone?
Frank Shearar,

Non conosco amico ... non lo so. Parte del motivo che sto chiedendo è perché non mi piace l'aspetto del mio codice asincrono.
Jeffery Thomas,

Sicuro. Finisci per scrivere il tuo codice in stile di passaggio di continuazione, il che non è terribilmente sorprendente. (Haskell ha la sua notazione esatta proprio per questo motivo:
lasciarti

Potresti essere interessato a questa implementazione di Promesse ObjC: github.com/couchdeveloper/RXPromise
e1985

0

Sembra un copout completo, ma non credo che ci sia una risposta giusta qui. Sono andato con il blocco di completamento semplicemente perché potrebbe essere ancora necessario eseguire la gestione degli errori nelle condizioni di successo quando si utilizzano i blocchi di successo / fallimento.

Penso che il codice finale sarà simile

[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
    }
}];

o semplicemente

[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
}];

Non il pezzetto di codice migliore e l'annidamento peggiora

[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
    }];
}];

Penso che andrò a scopare per un po '.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.