sukces: / niepowodzenie: bloki vs ukończenie: blok

23

Widzę dwa typowe wzorce bloków w Objective-C. Jedna to para sukcesów: / porażka: bloki, druga to pojedyncze zakończenie: blok.

Załóżmy na przykład, że mam zadanie, które zwróci obiekt asynchronicznie, a to zadanie może się nie powieść. Pierwszy wzór to -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure. Drugi wzór to -taskWithCompletion:(void (^)(id object, NSError *error))completion.

Brak powodzenia:

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

ukończenie:

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

Jaki jest preferowany wzór? Jakie są mocne i słabe strony? Kiedy użyjesz jednego nad drugim?

Jeffery Thomas
źródło
Jestem prawie pewien, że Objective-C ma obsługę wyjątków z rzutem / złapaniem, czy jest powód, dla którego nie możesz tego użyć?
FrustratedWithFormsDesigner
Każda z tych opcji pozwala na łączenie wywołań asynchronicznych, co nie daje wyjątków.
Frank Shearar
5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - idiomatic objc nie używa try / catch do kontroli przepływu.
Ant
1
Zastanów się nad przeniesieniem odpowiedzi z pytania na odpowiedź ... w końcu jest to odpowiedź (i możesz odpowiedzieć na własne pytania).
1
W końcu poddałem się presji otoczenia i przeniosłem swoją odpowiedź na rzeczywistą odpowiedź.
Jeffery Thomas,

Odpowiedzi:

8

Wywołanie zwrotne zakończenia (w przeciwieństwie do pary sukces / porażka) jest bardziej ogólne. Jeśli musisz przygotować jakiś kontekst przed zajęciem się statusem zwrotu, możesz to zrobić tuż przed klauzulą ​​„if (object)”. W przypadku powodzenia / niepowodzenia musisz zduplikować ten kod. Zależy to oczywiście od semantyki wywołania zwrotnego.


źródło
Nie można skomentować pierwotnego pytania ... Wyjątki nie są prawidłową kontrolą przepływu w celu c (no cóż, kakao) i nie powinny być stosowane jako takie. Zgłoszony wyjątek powinien zostać wyłapany tylko po to, aby zakończyć z gracją.
Tak, widzę to. Jeśli -task…mógłby zwrócić obiekt, ale obiekt nie jest w poprawnym stanie, nadal trzeba będzie obsługiwać błędy w warunkach powodzenia.
Jeffery Thomas
Tak, a jeśli blok nie jest na miejscu, ale jest przekazywany jako argument do kontrolera, musisz przerzucić dwa bloki. Może to być nudne, gdy oddzwanianie musi być przekazywane przez wiele warstw. Zawsze możesz go jednak podzielić / skomponować.
Nie rozumiem, jak moduł obsługi zakończenia jest bardziej ogólny. Ukończenie w zasadzie zamienia wiele parametrów metody w jeden - w postaci parametrów blokowych. Ponadto, czy rodzajowe oznacza lepsze? W MVC często zdarza się, że zduplikowany kod jest również kontrolerem widoku, co jest złem koniecznym z powodu rozdzielenia obaw. Nie sądzę jednak, że to powód, by trzymać się z dala od MVC.
Boon
@Boon Jednym z powodów, dla których uważam, że pojedynczy moduł obsługi jest bardziej ogólny, są przypadki, w których wolisz, aby sam system obsługi / moduł obsługi / blok określał, czy operacja się powiodła, czy nie. Rozważ przypadki częściowego sukcesu, w których prawdopodobnie masz obiekt z częściowymi danymi, a obiektem błędu jest błąd wskazujący, że nie wszystkie dane zostały zwrócone. Blok może zbadać same dane i sprawdzić, czy są wystarczające. Nie jest to możliwe w przypadku scenariusza binarnego wywołania zwrotnego powodzenia / niepowodzenia.
Travis,
8

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

! thensł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 thenjest 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 niloznacza, ż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 thenzwró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, asyncTaskCi 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, thenmoż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 cancelwiadomość 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.

CouchDeveloper
źródło
1
Otrzymuje nagrodę za najdłuższy jak dotąd brak odpowiedzi. Ale A za wysiłek :-)
Podróżujący Mężczyzna
3

Zdaję sobie sprawę, że to stare pytanie, ale muszę na nie odpowiedzieć, ponieważ moja odpowiedź jest inna niż inne.

Dla tych, którzy mówią, że to kwestia osobistych preferencji, muszę się nie zgodzić. Jest dobry, logiczny powód, aby preferować jedno od drugiego ...

W przypadku zakończenia, twojemu blokowi przekazane są dwa obiekty, jeden reprezentuje sukces, a drugi porażkę ... Więc co robisz, jeśli oba są zerowe? Co robisz, jeśli oba mają wartość? Są to pytania, których można uniknąć w czasie kompilacji i jako takie powinny być. Unikasz tych pytań, mając dwa oddzielne bloki.

Posiadanie osobnych bloków sukcesu i niepowodzenia sprawia, że ​​kod jest statystycznie weryfikowalny.


Pamiętaj, że wraz ze Swiftem sytuacja się zmienia. Możemy w nim zaimplementować pojęcie Eitherwyliczenia, aby zagwarantować, że pojedynczy blok uzupełniający zawiera obiekt lub błąd i musi mieć dokładnie jeden z nich. W przypadku Swift pojedynczy blok jest lepszy.

Daniel T.
źródło
1

Podejrzewam, że skończy się to osobistymi preferencjami ...

Ale wolę osobne bloki sukcesu / niepowodzenia. Lubię rozdzielać logikę sukcesu / niepowodzenia. Gdybyś zagnieździł sukces / porażkę, skończyłbyś z czymś, co byłoby bardziej czytelne (przynajmniej moim zdaniem).

Jako stosunkowo ekstremalny przykład takiego zagnieżdżenia, oto niektóre Ruby pokazujące ten wzór.

Frank Shearar
źródło
1
Widziałem zagnieżdżone łańcuchy obu. Myślę, że oba wyglądają okropnie, ale to moja osobista opinia.
Jeffery Thomas
1
Ale jak inaczej można łączyć asynchroniczne połączenia?
Frank Shearar
Nie znam człowieka… nie wiem. Jednym z powodów, dla których pytam, jest to, że nie podoba mi się wygląd mojego kodu asynchronicznego.
Jeffery Thomas
Pewnie. W końcu piszesz kod w stylu kontynuacji, co nie jest niczym zaskakującym. (Haskell ma swoją notację właśnie z tego powodu: pozwalając ci pisać w pozornie bezpośrednim stylu.)
Frank Shearar
Być może zainteresuje Cię ta implementacja ObjC Promises: github.com/couchdeveloper/RXPromise
e1985 13.09.13
0

To wydaje się kompletnym wyrzutem, ale nie sądzę, że tutaj jest poprawna odpowiedź. Poszedłem z blokiem ukończenia po prostu dlatego, że obsługa błędów może nadal wymagać wykonania w przypadku powodzenia przy użyciu bloków powodzenia / niepowodzenia.

Myślę, że końcowy kod będzie wyglądał podobnie

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

lub po prostu

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

Nie najlepsza część kodu i zagnieżdżanie go pogarsza się

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

Myślę, że przez jakiś czas będę szaleć.

Jeffery Thomas
źródło