Eu diria que, se a API fornece um manipulador de conclusão ou um par de blocos de sucesso / falha, é principalmente uma questão de preferência pessoal.
Ambas as abordagens têm prós e contras, embora haja apenas diferenças marginais.
Considere-se que existem também outras variantes, por exemplo, onde o um manipulador de conclusão pode ter apenas um parâmetro combinando o eventual resultado ou um erro potencial:
typedef void (^completion_t)(id result);
- (void) taskWithCompletion:(completion_t)completionHandler;
[self taskWithCompletion:^(id result){
if ([result isKindOfError:[NSError class]) {
NSLog(@"Error: %@", result);
}
else {
...
}
}];
O objetivo desta assinatura é que um manipulador de conclusão possa ser usado genericamente em outras APIs.
Por exemplo, em Categoria para NSArray, existe um método forEachApplyTask:completion:
que chama seqüencialmente uma tarefa para cada objeto e quebra o loop IFF, houve um erro. Como esse método também é assíncrono, ele também possui um manipulador de conclusão:
typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);
De fato, completion_t
conforme definido acima, é genérico e suficiente para lidar com todos os cenários.
No entanto, existem outros meios para uma tarefa assíncrona sinalizar sua notificação de conclusão ao site de chamada:
Promessas
Promessas, também chamadas de "Futuros", "Adiadas" ou "Atrasadas" representam o resultado final de uma tarefa assíncrona (consulte também: wiki Futuros e promessas ).
Inicialmente, uma promessa está no estado "pendente". Ou seja, seu "valor" ainda não foi avaliado e ainda não está disponível.
No Objective-C, uma Promessa seria um objeto comum que será retornado de um método assíncrono, como mostrado abaixo:
- (Promise*) doSomethingAsync;
! O estado inicial de uma promessa está "pendente".
Enquanto isso, as tarefas assíncronas começam a avaliar seu resultado.
Observe também que não há manipulador de conclusão. Em vez disso, a Promessa fornecerá um meio mais poderoso para que o site de chamadas possa obter o resultado final da tarefa assíncrona, que veremos em breve.
A tarefa assíncrona, que criou o objeto de promessa, DEVE eventualmente "resolver" sua promessa. Isso significa que, uma vez que uma tarefa pode ter êxito ou falhar, DEVE "cumprir" uma promessa passando o resultado avaliado ou DEVE "rejeitar" a promessa passando um erro indicando o motivo da falha.
! Uma tarefa deve eventualmente resolver sua promessa.
Quando uma promessa é resolvida, ela não pode mais mudar seu estado, incluindo seu valor.
! Uma promessa pode ser resolvida apenas uma vez .
Depois que uma promessa é resolvida, um site de chamada pode obter o resultado (se falhou ou teve êxito). Como isso é feito depende se a promessa é implementada usando o estilo síncrono ou assíncrono.
A Promise pode ser implementado em um síncrono ou assíncrono um modelo que leva a qualquer bloqueio , respectivamente, sem bloqueio semântica.
Em um estilo síncrono para recuperar o valor da promessa, um site de chamada usaria um método que bloqueará o encadeamento atual até que a promessa tenha sido resolvida pela tarefa assíncrona e o resultado final esteja disponível.
Em um estilo assíncrono, o site de chamada registraria retornos de chamada ou blocos de manipulador que são chamados imediatamente após a promessa ter sido resolvida.
Verificou-se que o estilo síncrono tem várias desvantagens significativas que efetivamente derrotam os méritos das tarefas assíncronas. Um artigo interessante sobre a implementação atualmente incorreta de "futuros" na lib padrão do C ++ 11 pode ser lida aqui: Promessas quebradas - futuros do C ++ 0x .
Como, no Objective-C, um site de chamadas obteria o resultado?
Bem, provavelmente é melhor mostrar alguns exemplos. Existem algumas bibliotecas que implementam uma promessa (veja os links abaixo).
No entanto, para os próximos trechos de código, usarei uma implementação específica de uma biblioteca Promise, disponível no GitHub RXPromise . Eu sou o autor de RXPromise.
As outras implementações podem ter uma API semelhante, mas pode haver diferenças pequenas e possivelmente sutis na sintaxe. RXPromise é uma versão Objective-C da especificação Promise / A + que define um padrão aberto para implementações robustas e interoperáveis de promessas em JavaScript.
Todas as bibliotecas promissoras listadas abaixo implementam o estilo assíncrono.
Existem diferenças bastante significativas entre as diferentes implementações. O RXPromise utiliza internamente a biblioteca de despacho, é totalmente seguro para threads, extremamente leve e também fornece vários recursos úteis adicionais, como cancelamento.
Um site de chamada obtém o resultado final da tarefa assíncrona por meio de "registradores" de manipuladores. A "especificação Promise / A +" define o método then
.
O método then
Com o RXPromise, tem a seguinte aparência:
promise.then(successHandler, errorHandler);
onde successHandler é um bloco que é chamado quando a promessa é "cumprida" e errorHandler é um bloco que é chamado quando a promessa é "rejeitada".
! then
é usado para obter o resultado final e definir um manipulador de sucesso ou erro.
No RXPromise, os blocos manipuladores têm a seguinte assinatura:
typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
O success_handler possui um resultado de parâmetro que é obviamente o resultado final da tarefa assíncrona. Da mesma forma, o error_handler possui um erro de parâmetro, que é o erro relatado pela tarefa assíncrona quando falhou.
Ambos os blocos têm um valor de retorno. O significado desse valor de retorno ficará claro em breve.
No RXPromise, then
é uma propriedade que retorna um bloco. Este bloco possui dois parâmetros, o bloco manipulador de sucesso e o bloco manipulador de erro. Os manipuladores devem ser definidos pelo site de chamada.
! Os manipuladores devem ser definidos pelo site de chamada.
Portanto, a expressão promise.then(success_handler, error_handler);
é uma forma curta de
then_block_t block promise.then;
block(success_handler, error_handler);
Podemos escrever um código ainda mais conciso:
doSomethingAsync
.then(^id(id result){
…
return @“OK”;
}, nil);
O código diz: "Execute doSomethingAsync, quando for bem-sucedido, depois execute o manipulador de sucesso".
Aqui, o manipulador de erros é o nil
que significa que, em caso de erro, ele não será tratado nesta promessa.
Outro fato importante é que chamar o bloco retornado da propriedade then
retornará uma promessa:
! then(...)
retorna uma promessa
Ao chamar o bloco retornado da propriedade then
, o "destinatário" retorna uma nova promessa, uma promessa filho . O receptor se torna a promessa dos pais .
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
O que isso significa?
Bem, devido a isso, podemos "encadear" tarefas assíncronas que efetivamente são executadas sequencialmente.
Além disso, o valor de retorno de qualquer manipulador se tornará o "valor" da promessa retornada. Portanto, se a tarefa tiver êxito com o resultado final @ “OK”, a promessa retornada será “resolvida” (que é “cumprida”) com o valor @ “OK”:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return @"OK";
}, nil);
...
assert([[returnedPromise get] isEqualToString:@"OK"]);
Da mesma forma, quando a tarefa assíncrona falhar, a promessa retornada será resolvida (que é "rejeitada") com um erro.
RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
return error;
});
...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);
O manipulador também pode retornar outra promessa. Por exemplo, quando esse manipulador executa outra tarefa assíncrona. Com esse mecanismo, podemos "encadear" tarefas assíncronas:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return asyncB(result);
}, nil);
! O valor de retorno de um bloco manipulador se torna o valor da promessa filho.
Se não houver promessa filho, o valor de retorno não terá efeito.
Um exemplo mais complexo:
Aqui, nós executamos asyncTaskA
, asyncTaskB
, asyncTaskC
e asyncTaskD
sequencialmente - e cada tarefa subseqüente leva o resultado da tarefa anterior como entrada:
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);
Essa "cadeia" também é chamada de "continuação".
Tratamento de erros
As promessas facilitam especialmente o manuseio de erros. Os erros serão "encaminhados" do pai para o filho se não houver um manipulador de erros definido na promessa do pai. O erro será encaminhado pela cadeia até que uma criança lide com isso. Assim, tendo a cadeia acima, podemos implementar o tratamento de erros apenas adicionando outra “continuação” que lida com um erro em potencial que pode ocorrer em qualquer lugar acima :
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;
});
Isso é semelhante ao estilo síncrono provavelmente mais familiar com o tratamento de exceções:
try {
id a = A();
id b = B(a);
id c = C(b);
id d = D(c);
// handle d
}
catch (NSError* error) {
NSLog(@“”Error: %@“, error);
}
As promessas em geral têm outros recursos úteis:
Por exemplo, tendo uma referência a uma promessa, then
é possível "registrar" quantos manipuladores desejar. No RXPromise, os manipuladores de registro podem ocorrer a qualquer momento e a partir de qualquer encadeamento, pois é totalmente seguro para encadeamento.
O RXPromise possui alguns recursos funcionais mais úteis, não exigidos pela especificação Promise / A +. Um é "cancelamento".
Descobriu-se que o "cancelamento" é uma característica valiosa e importante. Por exemplo, um site de chamada com uma referência a uma promessa pode enviar a cancel
mensagem para indicar que não está mais interessado no resultado final.
Imagine uma tarefa assíncrona que carrega uma imagem da web e que deve ser exibida em um controlador de exibição. Se o usuário se afastar do controlador de exibição atual, o desenvolvedor poderá implementar o código que envia uma mensagem de cancelamento para o imagePromise , que, por sua vez, aciona o manipulador de erros definido pela Operação de Solicitação HTTP, onde a solicitação será cancelada.
No RXPromise, uma mensagem de cancelamento será encaminhada apenas de um pai para seus filhos, mas não vice-versa. Ou seja, uma promessa "raiz" cancelará todas as promessas de crianças. Mas uma promessa infantil só cancelará o “ramo” onde é o pai. A mensagem de cancelamento também será encaminhada às crianças se uma promessa já tiver sido resolvida.
Uma tarefa assíncrona pode -se registar manipulador para a sua própria promessa, e, assim, pode detectar quando alguém o cancelou. Pode então parar prematuramente de executar uma tarefa possivelmente longa e cara.
Aqui estão algumas outras implementações de Promises no Objective-C encontradas no 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 minha própria implementação: RXPromise .
Esta lista provavelmente não está completa!
Ao escolher uma terceira biblioteca para o seu projeto, verifique cuidadosamente se a implementação da biblioteca segue os pré-requisitos listados abaixo:
Uma biblioteca de promessas confiável DEVE ser segura para threads!
É tudo sobre processamento assíncrono, e queremos utilizar várias CPUs e executar em diferentes threads simultaneamente, sempre que possível. Tenha cuidado, a maioria das implementações não é segura para threads!
Os manipuladores devem ser chamados de forma assíncrona, no que diz respeito ao local da chamada! Sempre e não importa o que aconteça!
Qualquer implementação decente também deve seguir um padrão muito rigoroso ao chamar as funções assíncronas. Muitos implementadores tendem a "otimizar" o caso, em que um manipulador será chamado de forma síncrona quando a promessa já estiver resolvida quando o manipulador será registrado. Isso pode causar todos os tipos de problemas. Consulte Não liberte o Zalgo! .
Também deve haver um mecanismo para cancelar uma promessa.
A possibilidade de cancelar uma tarefa assíncrona geralmente se torna um requisito com alta prioridade na análise de requisitos. Caso contrário, com certeza haverá uma solicitação de aprimoramento do usuário algum tempo depois após o lançamento do aplicativo. O motivo deve ser óbvio: qualquer tarefa que possa parar ou demorar muito para terminar deve ser cancelável pelo usuário ou por um tempo limite. Uma biblioteca de promessas decente deve apoiar o cancelamento.