Powinieneś użyć obu. Chodzi o to, aby zdecydować, kiedy użyć każdego z nich .
Istnieje kilka scenariuszy, w których wyjątki są oczywistym wyborem :
W niektórych sytuacjach nie możesz nic zrobić z kodem błędu i po prostu musisz obsłużyć go na wyższym poziomie w stosie wywołań , zwykle po prostu zarejestruj błąd, wyświetl coś użytkownikowi lub zamknij program. W takich przypadkach kody błędów wymagałyby ręcznego uzupełniania kodów błędów na poziomie, co jest oczywiście znacznie łatwiejsze w przypadku wyjątków. Chodzi o to, że dotyczy to nieoczekiwanych i trudnych do rozwiązania sytuacji.
Jednak w przypadku sytuacji 1 (gdy dzieje się coś nieoczekiwanego i nie do obsłużenia, po prostu nie chcesz tego zarejestrować), wyjątki mogą być pomocne, ponieważ możesz dodać informacje kontekstowe . Na przykład, jeśli otrzymam SqlException w moich pomocnikach danych niższego poziomu, będę chciał złapać ten błąd na niskim poziomie (gdzie znam polecenie SQL, które spowodowało błąd), aby móc przechwycić te informacje i ponownie przesłać z dodatkowymi informacjami . Zwróć uwagę na magiczne słowo: rzuć ponownie, a nie połykać .
Pierwsza zasada obsługi wyjątków: nie połykaj wyjątków . Zwróć również uwagę, że mój wewnętrzny zaczep nie musi niczego rejestrować, ponieważ zewnętrzny zaczep będzie zawierał cały ślad stosu i może go rejestrować.
W niektórych sytuacjach masz sekwencję poleceń i jeśli którekolwiek z nich zawiedzie , powinieneś wyczyścić / pozbyć się zasobów (*), niezależnie od tego, czy jest to sytuacja nieodwracalna (która powinna zostać wyrzucona), czy sytuacja dająca się naprawić (w takim przypadku możesz obsłużyć lokalnie lub w kodzie dzwoniącego, ale nie potrzebujesz wyjątków). Oczywiście znacznie łatwiej jest umieścić wszystkie te polecenia za jednym razem, zamiast testować kody błędów po każdej metodzie i czyścić / usuwać w ostatnim bloku. Zwróć uwagę, że jeśli chcesz, aby błąd się wypalił (co prawdopodobnie jest tym, czego chcesz), nie musisz go nawet łapać - po prostu użyj ostatniego do czyszczenia / usuwania - powinieneś używać tylko catch / retrow, jeśli chcesz aby dodać informacje kontekstowe (patrz punktor 2).
Jednym z przykładów może być sekwencja instrukcji SQL wewnątrz bloku transakcji. Ponownie, jest to również sytuacja „nie do rozwiązania”, nawet jeśli zdecydujesz się ją wcześnie złapać (lecz lokalnie zamiast bulgotać do góry), nadal jest to fatalna sytuacja, w której najlepszym wynikiem jest przerwanie wszystkiego lub przynajmniej przerwanie dużego część procesu.
(*) To jest podobne do on error goto
tego, którego używaliśmy w starym Visual Basicu
W konstruktorach możesz zgłaszać tylko wyjątki.
To powiedziawszy, we wszystkich innych sytuacjach, w których zwracasz informacje, na temat których dzwoniący MOŻE / POWINIEN podjąć jakąś akcję , użycie kodów powrotnych jest prawdopodobnie lepszą alternatywą. Obejmuje to wszystkie oczekiwane „błędy” , ponieważ prawdopodobnie powinny być obsługiwane przez bezpośredniego wywołującego i prawie nie będą musiały być zwiększane zbyt wiele poziomów w stosie.
Oczywiście zawsze można traktować oczekiwane błędy jako wyjątki, a następnie natychmiast przechwytywać jeden poziom wyżej, a także można objąć każdą linię kodu w try catch i podjąć działania dla każdego możliwego błędu. IMO, to zły projekt, nie tylko dlatego, że jest dużo bardziej szczegółowy, ale szczególnie dlatego, że możliwe wyjątki, które mogą zostać wyrzucone, nie są oczywiste bez czytania kodu źródłowego - a wyjątki mogą być wyrzucane z dowolnej głębokiej metody, tworząc niewidoczne goto . Łamią strukturę kodu, tworząc wiele niewidocznych punktów wyjścia, które sprawiają, że kod jest trudny do odczytania i sprawdzenia. Innymi słowy, nigdy nie powinieneś używać wyjątków jako kontroli przepływu, ponieważ innym byłoby to trudne do zrozumienia i utrzymania. Zrozumienie wszystkich możliwych przepływów kodu do testowania może być nawet trudne.
Ponownie: w celu prawidłowego oczyszczenia / usunięcia możesz użyć try-last bez łapania czegokolwiek .
Najpopularniejsza krytyka dotycząca kodów powrotnych mówi, że „ktoś mógłby zignorować kody błędów, ale w tym samym sensie ktoś może również połknąć wyjątki. Zła obsługa wyjątków jest łatwa w obu metodach. Jednak pisanie dobrego programu opartego na kodzie błędów jest nadal znacznie łatwiejsze niż pisanie programu opartego na wyjątkach . A jeśli z jakiegoś powodu zdecydujesz się zignorować wszystkie błędy (stare on error resume next
), możesz to łatwo zrobić za pomocą kodów powrotu i nie możesz tego zrobić bez wielu standardowych schematów try-catch.
Drugą najpopularniejszą krytyką dotyczącą kodów powrotnych jest to, że „trudno jest wypalić” - ale to dlatego, że ludzie nie rozumieją, że wyjątki dotyczą sytuacji, których nie można odzyskać, podczas gdy kody błędów nie.
Wybór między wyjątkami a kodami błędów to szara strefa. Jest nawet możliwe, że musisz uzyskać kod błędu z jakiejś metody biznesowej wielokrotnego użytku, a następnie zdecydujesz się opakować go w wyjątek (prawdopodobnie dodając informacje) i pozwolić mu się rozwinąć. Ale błędem projektowym jest założenie, że WSZYSTKIE błędy powinny być odrzucane jako wyjątki.
Podsumowując:
Lubię używać wyjątków, gdy mam nieoczekiwaną sytuację, w której nie ma zbyt wiele do zrobienia, a zwykle chcemy przerwać duży blok kodu lub nawet całą operację lub program. To jest jak stare „przy błędzie goto”.
Lubię używać kodów powrotnych, gdy spodziewałem się sytuacji, w których kod dzwoniącego może / powinien podjąć jakieś działanie. Dotyczy to większości metod biznesowych, interfejsów API, walidacji i tak dalej.
Ta różnica między wyjątkami a kodami błędów jest jedną z zasad projektowania języka GO, który używa wyrażenia „panika” w przypadku krytycznych nieoczekiwanych sytuacji, podczas gdy zwykłe oczekiwane sytuacje są zwracane jako błędy.
Jednak w przypadku GO pozwala również na wiele zwracanych wartości , co jest czymś, co bardzo pomaga w używaniu kodów powrotu, ponieważ możesz jednocześnie zwrócić błąd i coś innego. W C # / Javie możemy to osiągnąć bez parametrów, krotek lub (moich ulubionych) typów generycznych, które w połączeniu z wyliczeniami mogą zapewnić wywołującemu wyraźne kody błędów:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}
var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
// do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
order = result.Entity; // etc...
Jeśli dodam nowy możliwy zwrot w mojej metodzie, mogę nawet sprawdzić wszystkich wywołujących, jeśli na przykład pokrywają tę nową wartość w instrukcji switch. Naprawdę nie możesz tego zrobić z wyjątkami. Korzystając z kodów zwrotnych, zazwyczaj znasz z wyprzedzeniem wszystkie możliwe błędy i je testujesz. Z wyjątkami zazwyczaj nie wiesz, co może się stać. Zawijanie wyliczeń wewnątrz wyjątków (zamiast Generics) jest alternatywą (o ile jest jasne, jaki typ wyjątków będzie generować każda metoda), ale IMO to nadal zły projekt.