Powiedziałbym, że to, czy interfejs API zapewnia jedną procedurę obsługi zakończenia, czy parę bloków sukcesu / niepowodzenia, jest przede wszystkim kwestią osobistych preferencji.
Oba podejścia mają zalety i wady, choć różnice są tylko nieznacznie.
Weź pod uwagę, że istnieją również inne warianty, na przykład gdy jeden moduł obsługi zakończenia może mieć tylko jeden parametr łączący ostateczny wynik lub potencjalny błąd:
typedef void (^completion_t)(id result);
- (void) taskWithCompletion:(completion_t)completionHandler;
[self taskWithCompletion:^(id result){
if ([result isKindOfError:[NSError class]) {
NSLog(@"Error: %@", result);
}
else {
...
}
}];
Celem tego podpisu jest to, że moduł obsługi zakończenia może być używany ogólnie w innych interfejsach API.
Na przykład w kategorii dla NSArray istnieje metoda, forEachApplyTask:completion:
która sekwencyjnie wywołuje zadanie dla każdego obiektu i przerywa IFF pętli, wystąpił błąd. Ponieważ ta metoda sama w sobie jest również asynchroniczna, ma również moduł obsługi zakończenia:
typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);
W rzeczywistości, completion_t
jak zdefiniowano powyżej, jest wystarczająco ogólny i wystarczający do obsługi wszystkich scenariuszy.
Istnieją jednak inne sposoby wykonywania zadania asynchronicznego w celu zasygnalizowania powiadomienia o zakończeniu do strony wywołującej:
Obietnice
Obietnice, zwane także „kontraktami terminowymi”, „odroczonymi” lub „opóźnionymi” reprezentują ostateczny wynik zadania asynchronicznego (patrz także: wiki Kontrakty terminowe i obietnice ).
Początkowo obietnica jest w stanie „w toku”. Oznacza to, że jego „wartość” nie została jeszcze oceniona i nie jest jeszcze dostępna.
W Celu C obietnica byłaby zwykłym przedmiotem, który zostanie zwrócony z metody asynchronicznej, jak pokazano poniżej:
- (Promise*) doSomethingAsync;
! Początkowy stan obietnicy jest „w toku”.
Tymczasem zadania asynchroniczne zaczynają oceniać swój wynik.
Zauważ też, że nie ma modułu obsługi zakończenia. Zamiast tego obietnica zapewni skuteczniejsze środki, dzięki którym strona wywołująca może uzyskać ostateczny wynik zadania asynchronicznego, co wkrótce zobaczymy.
Zadanie asynchroniczne, które utworzyło obiekt obietnicy, MUSI ostatecznie „rozwiązać” obietnicę. Oznacza to, że ponieważ zadanie może albo zakończyć się sukcesem, albo porażką, MUSI albo „spełnić” obietnicę przekazując mu oceniany wynik, albo MUSI „odrzucić” przekazaną obietnicę błąd wskazujący przyczynę niepowodzenia.
! Zadanie musi ostatecznie spełnić obietnicę.
Kiedy obietnica została rozwiązana, nie może już zmienić swojego stanu, w tym jego wartości.
! Obietnicę można rozwiązać tylko raz .
Po rozwiązaniu obietnicy strona wywołująca może uzyskać wynik (niezależnie od tego, czy się nie powiodła, czy też powiodła). Sposób realizacji zależy od tego, czy obietnica jest realizowana przy użyciu stylu synchronicznego, czy asynchronicznego.
Obietnicę można wdrożyć w stylu synchronicznym lub asynchronicznym, co prowadzi do blokowania lub nieblokowania semantyki.
W stylu synchronicznym w celu odzyskania wartości obietnicy strona wywołująca użyłaby metody, która zablokuje bieżący wątek, dopóki obietnica nie zostanie rozwiązana przez zadanie asynchroniczne, a ostateczny wynik będzie dostępny.
W stylu asynchronicznym strona wywołująca rejestruje połączenia zwrotne lub bloki obsługi, które są wywoływane natychmiast po rozstrzygnięciu obietnicy.
Okazało się, że styl synchroniczny ma wiele istotnych wad, które skutecznie pokonują zalety zadań asynchronicznych. Interesujący artykuł na temat obecnie wadliwej implementacji „futures” w standardowej bibliotece C ++ 11 można przeczytać tutaj: Złamane obietnice - C ++ 0x futures .
Jak w Objective-C strona wywołująca uzyska wynik?
Cóż, prawdopodobnie najlepiej jest pokazać kilka przykładów. Istnieje kilka bibliotek, które realizują obietnicę (patrz linki poniżej).
Jednak w przypadku kolejnych fragmentów kodu użyję konkretnej implementacji biblioteki Promise, dostępnej w GitHub RXPromise . Jestem autorem RXPromise.
Inne implementacje mogą mieć podobny interfejs API, ale mogą występować niewielkie i prawdopodobnie subtelne różnice w składni. RXPromise to wersja Objective-C specyfikacji Promise / A +, która definiuje otwarty standard dla solidnych i interoperacyjnych implementacji obietnic w JavaScript.
Wszystkie wymienione poniżej biblioteki obietnic implementują styl asynchroniczny.
Istnieją dość znaczące różnice między różnymi implementacjami. RXPromise wewnętrznie wykorzystuje bibliotekę wysyłkową, jest w pełni bezpieczny dla wątków, wyjątkowo lekki, a także zapewnia szereg dodatkowych przydatnych funkcji, takich jak anulowanie.
Witryna wywołująca uzyskuje ostateczny wynik zadania asynchronicznego poprzez „rejestrację” procedur obsługi. „Specyfikacja Promise / A +” określa metodę then
.
Metoda then
Z RXPromise wygląda to następująco:
promise.then(successHandler, errorHandler);
gdzie SuccessHandler to blok, który jest wywoływany, gdy obietnica została „spełniona”, a errorHandler to blok, który jest wywoływany, gdy obietnica została „odrzucona”.
! then
służy do uzyskania ostatecznego wyniku i do określenia sukcesu lub procedury obsługi błędów.
W RXPromise bloki procedury obsługi mają następującą sygnaturę:
typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
Program obsługi sukcesu ma wynik parametru, który jest oczywiście ostatecznym wynikiem zadania asynchronicznego. Podobnie moduł obsługi błędów zawiera błąd parametru, który jest błędem zgłaszanym przez zadanie asynchroniczne w przypadku niepowodzenia.
Oba bloki mają wartość zwracaną. O co chodzi w tej wartości zwrotnej, wkrótce stanie się jasne.
W RXPromise then
jest właściwością, która zwraca blok. Ten blok ma dwa parametry: blok obsługi sukcesu i blok obsługi błędów. Procedury obsługi muszą być zdefiniowane przez stronę wywołującą.
! Procedury obsługi muszą być zdefiniowane przez stronę wywołującą.
Tak więc wyrażenie promise.then(success_handler, error_handler);
jest krótką formą
then_block_t block promise.then;
block(success_handler, error_handler);
Możemy napisać jeszcze bardziej zwięzły kod:
doSomethingAsync
.then(^id(id result){
…
return @“OK”;
}, nil);
Kod brzmi: „Wykonaj doSomethingAsync, gdy się powiedzie, a następnie uruchom moduł obsługi sukcesu”.
W tym przypadku procedura obsługi błędów nil
oznacza, że w przypadku błędu nie będzie obsługiwana w tej obietnicy.
Innym ważnym faktem jest to, że wywołanie bloku zwróconego z właściwości then
zwróci obietnicę:
! then(...)
zwraca obietnicę
Podczas wywoływania bloku zwróconego z właściwości then
„odbiorca” zwraca nową obietnicę, obietnicę podrzędną . Odbiorca staje się obietnicą rodzica .
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
Co to znaczy?
Dzięki temu możemy „łączyć” zadania asynchroniczne, które skutecznie są wykonywane sekwencyjnie.
Ponadto wartość zwracana przez jedną z procedur obsługi stanie się „wartością” zwróconej obietnicy. Tak więc, jeśli zadanie powiedzie się z ostatecznym wynikiem @ „OK”, zwrócona obietnica zostanie „rozwiązana” (to znaczy „spełniona”) o wartości @ „OK”:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return @"OK";
}, nil);
...
assert([[returnedPromise get] isEqualToString:@"OK"]);
Podobnie, gdy zadanie asynchroniczne nie powiedzie się, zwrócona obietnica zostanie rozwiązana (czyli „odrzucona”) z błędem.
RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
return error;
});
...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);
Przewodnik może również zwrócić inną obietnicę. Na przykład, gdy ten moduł obsługi wykonuje inne zadanie asynchroniczne. Za pomocą tego mechanizmu możemy „łączyć” zadania asynchroniczne:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return asyncB(result);
}, nil);
! Zwracana wartość bloku obsługi staje się wartością obietnicy podrzędnej.
Jeśli nie ma przyrzeczenia podrzędnego, wartość zwracana nie ma wpływu.
Bardziej złożony przykład:
Tutaj wykonujemy asyncTaskA
, asyncTaskB
, asyncTaskC
i asyncTaskD
kolejno - i każdego kolejnego zadania wykonuje wynik z poprzedniego zadania jako dane wejściowe:
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);
Taki „łańcuch” nazywa się również „kontynuacją”.
Obsługa błędów
Obietnice sprawiają, że szczególnie łatwo jest obsługiwać błędy. Błędy będą „przekazywane” od rodzica do dziecka, jeśli w obietnicy nadrzędnej nie zdefiniowano modułu obsługi błędów. Błąd będzie przekazywany w górę łańcucha, dopóki dziecko go nie obsłuży. Zatem mając powyższy łańcuch, możemy zaimplementować obsługę błędów po prostu dodając kolejną „kontynuację”, która dotyczy potencjalnego błędu, który może wystąpić gdziekolwiek powyżej :
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;
});
Jest to podobne do prawdopodobnie bardziej znanego stylu synchronicznego z obsługą wyjątków:
try {
id a = A();
id b = B(a);
id c = C(b);
id d = D(c);
// handle d
}
catch (NSError* error) {
NSLog(@“”Error: %@“, error);
}
Obietnice mają ogólnie inne przydatne funkcje:
Na przykład, mając odniesienie do obietnicy, then
można „zarejestrować” dowolną liczbę osób zajmujących się obsługą. W RXPromise rejestrowanie procedur obsługi może nastąpić w dowolnym momencie i z dowolnego wątku, ponieważ jest w pełni bezpieczny dla wątków.
RXPromise ma kilka bardziej użytecznych funkcji, które nie są wymagane przez specyfikację Promise / A +. Jednym z nich jest „anulowanie”.
Okazało się, że „anulowanie” jest nieocenioną i ważną cechą. Na przykład strona wywołująca zawierająca odniesienie do obietnicy może wysłać do niej cancel
wiadomość w celu wskazania, że nie jest już zainteresowany ostatecznym wynikiem.
Wyobraź sobie asynchroniczne zadanie, które ładuje obraz z sieci i które powinno być wyświetlane w kontrolerze widoku. Jeśli użytkownik odejdzie od bieżącego kontrolera widoku, programista może zaimplementować kod, który wysyła komunikat anulowania do imagePromise , co z kolei uruchamia procedurę obsługi błędów zdefiniowaną przez Operację żądania HTTP, w której żądanie zostanie anulowane.
W RXPromise komunikat anulowania będzie przekazywany tylko od rodzica do jego dzieci, ale nie odwrotnie. Oznacza to, że obietnica „root” anuluje wszystkie obietnice dla dzieci. Ale obietnica dziecięca anuluje „oddział”, w którym jest rodzicem. Wiadomość anulowania zostanie również przekazana dzieciom, jeśli obietnica została już rozwiązana.
Zadanie asynchroniczne może samo zarejestrować moduł obsługi dla własnej obietnicy, a tym samym może wykryć, kiedy ktoś go anulował. Może wtedy przedwcześnie przestać wykonywać możliwie długie i kosztowne zadania.
Oto kilka innych implementacji Obietnic w Objective-C znalezionych na 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
i moja własna implementacja: RXPromise .
Ta lista prawdopodobnie nie jest kompletna!
Wybierając trzecią bibliotekę do swojego projektu, sprawdź dokładnie, czy implementacja biblioteki spełnia poniższe wymagania wstępne:
Niezawodna biblioteka obietnic MUSI być bezpieczna dla wątków!
Chodzi o przetwarzanie asynchroniczne, a my chcemy wykorzystywać wiele procesorów i wykonywać je jednocześnie w różnych wątkach, gdy tylko jest to możliwe. Bądź ostrożny, większość implementacji nie jest bezpieczna dla wątków!
Procedury obsługi MUSZĄ być wywoływane asynchronicznie w odniesieniu do strony wywołującej! Zawsze i bez względu na wszystko!
Każda przyzwoita implementacja powinna również przestrzegać bardzo ścisłego wzorca podczas wywoływania funkcji asynchronicznych. Wielu implementatorów ma tendencję do „optymalizacji” przypadku, w którym moduł obsługi zostanie wywołany synchronicznie, gdy obietnica zostanie już rozwiązana, gdy moduł obsługi zarejestruje się. Może to powodować różnego rodzaju problemy. Zobacz Nie uwalniaj Zalgo! .
Powinien również istnieć mechanizm anulowania obietnicy.
Możliwość anulowania zadania asynchronicznego często staje się wymaganiem o wysokim priorytecie w analizie wymagań. Jeśli nie, to na pewno zostanie złożone żądanie rozszerzenia przez użytkownika jakiś czas później po wydaniu aplikacji. Powód powinien być oczywisty: każde zadanie, które może zostać zatrzymane lub potrwać zbyt długo, powinno zostać anulowane przez użytkownika lub po przekroczeniu limitu czasu. Biblioteka godnych obietnic powinna wspierać anulowanie.