Argumenty przeciwko eliminacji błędów

35

W jednym z naszych projektów znalazłem taki kod:

    SomeClass QueryServer(string args)
    {
        try
        {
            return SomeClass.Parse(_server.Query(args));
        }
        catch (Exception)
        {
            return null;
        }
    }

O ile rozumiem, pomijanie takich błędów jest złą praktyką, ponieważ niszczy przydatne informacje z wyjątku oryginalnego serwera i sprawia, że ​​kod jest kontynuowany, kiedy powinien się zakończyć.

Kiedy należy całkowicie wyeliminować wszystkie takie błędy?

użytkownik626528
źródło
13
Eric Lippert napisał świetny post na blogu zatytułowany irytujące wyjątki, w którym klasyfikuje wyjątki do 4 różnych kategorii i sugeruje, jakie podejście jest właściwe dla każdej z nich. Wierzę, że napisany z perspektywy C #, ale dotyczy różnych języków.
Damien_The_Unbeliever
3
Uważam, że kod narusza zasadę „szybkiego działania”. Istnieje duże prawdopodobieństwo, że ukryty jest w nim prawdziwy błąd.
Euforia
4
Za dużo łapiesz. Będziesz także łapał błędy, takie jak NRE, indeks tablicy poza zakresem, błąd konfiguracji, ... To uniemożliwia znalezienie tych błędów i będą one stale powodować uszkodzenia.
usr

Odpowiedzi:

52

Wyobraź sobie kod z tysiącami plików korzystających z wielu bibliotek. Wyobraź sobie, że wszystkie są tak zakodowane.

Wyobraź sobie na przykład, że aktualizacja twojego serwera powoduje zniknięcie jednego pliku konfiguracyjnego; a teraz wszystko, co masz, to ślad stosu, wyjątek wskaźnika zerowego, gdy próbujesz użyć tej klasy: jak byś to rozwiązał? Może to potrwać godziny, w których co najmniej rejestrowanie nieprzetworzonego śladu stosu pliku nie znaleziono [ścieżka pliku] może umożliwić natychmiastowe rozwiązanie problemu.

Albo jeszcze gorzej: awaria jednej z bibliotek używanych po aktualizacji, która powoduje awarię kodu w późniejszym czasie. Jak możesz to prześledzić z powrotem do biblioteki?

Nawet bez solidnej obsługi błędów

throw new IllegalStateException("THIS SHOULD NOT HAPPENING")

lub

LOGGER.error("[method name]/[arguments] should not be there")

może zaoszczędzić wiele godzin.

Ale są przypadki, w których naprawdę możesz zignorować wyjątek i zwrócić wartość null w ten sposób (lub nic nie robić). Zwłaszcza jeśli zintegrujesz się z jakimś źle zaprojektowanym starszym kodem i ten wyjątek jest oczekiwany jako normalny przypadek.

W rzeczywistości, robiąc to, powinieneś po prostu zastanawiać się, czy naprawdę ignorujesz wyjątek lub „odpowiednio obchodzisz się” ze swoimi potrzebami. Jeśli zwracanie wartości null „prawidłowo obsługuje” wyjątek w danym przypadku, zrób to. I dodaj komentarz, dlaczego to jest właściwe.

W większości przypadków należy stosować najlepsze praktyki, może 80%, może 99%, ale zawsze znajdziesz jeden przypadek skrajny, w którym nie mają zastosowania. W takim przypadku zostaw komentarz, dlaczego nie stosujesz się do praktyki dla innych (a nawet siebie), którzy przeczytają Twój kod kilka miesięcy później.

Walfrat
źródło
7
+1 za wyjaśnienie, dlaczego Twoim zdaniem musisz to zrobić w komentarzu. Bez wyjaśnienia połknięcie wyjątku zawsze budziłoby mi w głowie dzwonki alarmowe.
jpmc26,
Mój problem polega na tym, że komentarz utknął w tym kodzie. Jako konsument funkcji mogę nigdy nie zobaczyć tego komentarza.
corsiKa
@ jpmc26 Jeśli wyjątek zostanie obsłużony (na przykład przez zwrócenie specjalnej wartości), wcale nie jest to połykanie wyjątku.
Deduplicator
2
@corsiKa konsument nie musi znać szczegółów implementacji, jeśli funkcja wykonuje swoje zadanie poprawnie. może są tego niektóre w bibliotekach Apache lub wiosną i nie wiesz o tym.
Walfrat
@Deduplikator tak, ale użycie haczyka jako części algorytmu też nie powinno być dobrą
praktyką
14

Są przypadki, w których ten wzorzec jest użyteczny - ale są one zwykle używane, gdy wygenerowany wyjątek nigdy nie powinien był być (tj. Gdy wyjątki są używane do normalnego zachowania).

Na przykład wyobraź sobie, że masz klasę, która otwiera plik i przechowuje go w zwracanym obiekcie. Jeśli plik nie istnieje, możesz uznać, że nie jest to przypadek błędu, zwracasz null i pozwalasz użytkownikowi zamiast tego utworzyć nowy plik. Metoda otwierania pliku może zgłosić tutaj wyjątek wskazujący brak pliku, więc przechwycenie go w ciszy może być prawidłową sytuacją.

Jednak nie często chcesz to zrobić, a jeśli kod jest zaśmiecony takim wzorcem, chciałbyś sobie z tym poradzić. Przynajmniej spodziewałbym się, że tak cichy haczyk napisze wiersz dziennika informujący, że tak się stało (masz śledzenie, prawda) i komentarz wyjaśniający to zachowanie.

gbjbaanb
źródło
2
„używane, gdy wygenerowany wyjątek nigdy nie powinien być”, nie ma czegoś takiego. PUNKT wyjątku polega na sygnalizowaniu, że coś poszło nie tak.
Euforia
3
@Euphoric Ale czasami autor biblioteki nie zna intencji końcowego użytkownika tej biblioteki. „Okropnie zły” jednej osoby może być łatwym do odzyskania stanem innej osoby.
Simon B
2
@Euforia, zależy to od tego, co Twój język uważa za „okropnie źle”: ludzie Pythona lubią wyjątki od rzeczy, które są bardzo bliskie kontroli przepływu w (np.) C ++. Zwrócenie wartości null może być obronne w niektórych sytuacjach - np. Czytanie z pamięci podręcznej, w której przedmioty mogą zostać nagle i nieprzewidywalnie eksmitowane.
Matt Krause,
10
@Euphoric, „Nie znaleziono pliku nie jest normalnym wyjątkiem. Najpierw sprawdź, czy plik istnieje, jeśli istnieje możliwość, że nie istnieje”. Nie! Nie! Nie! Nie! To klasyczne złe myślenie o operacjach atomowych. Plik może zostać usunięty między tobą, sprawdzając, czy istnieje i uzyskując do niego dostęp. Jedynym prawidłowym sposobem radzenia sobie z takimi sytuacjami jest dostęp do nich i obsługa wyjątków, jeśli wystąpią, np. Przez zwrócenie, nulljeśli jest to najlepsze, co może zaoferować wybrany język.
David Arno
3
@Euphoric: Więc napisz kod, aby poradzić sobie z niemożnością uzyskania dostępu do pliku co najmniej dwa razy? To narusza DRY, a także niewykorzystany kod jest uszkodzony.
Deduplicator
7

Jest to w 100% zależne od kontekstu. Jeśli osoba wywołująca taką funkcję ma obowiązek wyświetlania lub rejestrowania błędów, jest to oczywiście nonsens. Jeśli osoba dzwoniąca zignoruje wszystkie komunikaty o błędach lub wyjątkach, zwrócenie wyjątku lub jego komunikatu nie ma większego sensu. Gdy wymaga się, aby kod zakończył się tylko bez podawania przyczyny, wówczas wywołanie

   if(QueryServer(args)==null)
      TerminateProgram();

prawdopodobnie byłby wystarczający. Musisz rozważyć, czy utrudni to znalezienie przyczyny błędu przez użytkownika - w takim przypadku jest to niewłaściwa forma obsługi błędów. Jednak jeśli kod wywołujący wygląda tak

  result = QueryServer(args);
  if(result!=null)
      DoSomethingMeaningful(result);
  // else ??? Mask the error and let the user run into trouble later

wtedy powinieneś spierać się z deweloperem podczas przeglądania kodu. Jeśli ktoś wdroży taką formę obsługi bezbłędnej tylko dlatego, że jest zbyt leniwy, aby dowiedzieć się o prawidłowych wymaganiach, kod ten nie powinien zostać wprowadzony do produkcji, a jego autor powinien zostać pouczony o możliwych konsekwencjach takiego lenistwa .

Doktor Brown
źródło
5

W latach, które spędziłem programując i rozwijając systemy, były tylko dwie sytuacje, w których uważałem ten wzorzec za użyteczny (w obu przypadkach eliminacja zawierała również rejestrowanie zgłoszonego wyjątku, nie uważam zwykłego chwytania i nullpowrotu za dobrą praktykę ).

Dwie sytuacje są następujące:

1. Gdy wyjątek nie został uznany za stan wyjątkowy

Dzieje się tak, gdy wykonujesz operację na niektórych danych, które mogą wyrzucić, wiesz, że mogą one wyrzucić, ale nadal chcesz, aby aplikacja nadal działała, ponieważ nie potrzebujesz przetworzonych danych. Jeśli je otrzymasz, to dobrze, jeśli nie, to także dobrze.

Mogą pojawić się pewne opcjonalne atrybuty klasy.

2. Kiedy udostępniasz nową (lepszą, szybszą?) Implementację biblioteki przy użyciu interfejsu już używanego w aplikacji

Wyobraź sobie, że masz aplikację korzystającą ze starej biblioteki, która nie zgłaszała wyjątków, ale zwracała nullbłąd. Utworzyłeś więc adapter dla tej biblioteki, kopiując prawie oryginalne API biblioteki, i używasz tego nowego (wciąż nie rzucającego) interfejsu w swojej aplikacji i nullsam przeprowadzasz kontrole.

Nadchodzi nowa wersja biblioteki, a może zupełnie inna biblioteka oferująca tę samą funkcjonalność, która zamiast zwracać nulls, generuje wyjątki i chcesz z niej korzystać.

Nie chcesz przeciekać wyjątków od głównej aplikacji, więc wyłącz je i zaloguj w utworzonym adapterze, aby zawinąć tę nową zależność.


Pierwszy przypadek nie stanowi problemu, jest to pożądane zachowanie kodu. Jednak w drugiej sytuacji, jeśli wszędzie nullzwracana wartość adaptera biblioteki naprawdę oznacza błąd, refaktoryzacja interfejsu API w celu wygenerowania wyjątku i wyłapanie go zamiast sprawdzania nullmoże być (i zwykle jest to kodowo) dobrym pomysłem.

Osobiście używam tłumienia wyjątków tylko w pierwszym przypadku. Użyłem go tylko w drugim przypadku, gdy nie mieliśmy budżetu, aby reszta aplikacji działała z wyjątkami zamiast nulls.

Andy
źródło
-1

Chociaż logiczne wydaje się stwierdzenie, że programy powinny wychwytywać tylko wyjątki, które potrafią obsłużyć, i nie mogą wiedzieć, jak radzić sobie z wyjątkami, których programista nie oczekiwał, takie stwierdzenie ignoruje fakt, że wiele operacji może zakończyć się niepowodzeniem prawie nieograniczona liczba sposobów, które nie wywołują skutków ubocznych, oraz że w wielu przypadkach właściwe postępowanie w przypadku większości takich awarii będzie identyczne; dokładne szczegóły awarii będą nieistotne, w związku z czym nie powinno mieć znaczenia, czy programista je przewidział.

Jeśli na przykład funkcja ma na celu odczytanie pliku dokumentu do odpowiedniego obiektu i albo utworzenie nowego okna dokumentu, aby pokazać ten obiekt, albo zgłoszenie użytkownikowi, że pliku nie można odczytać, próba załadowania niepoprawny plik dokumentu nie powinien powodować awarii aplikacji - powinien zamiast tego wyświetlać komunikat wskazujący na problem, ale pozwolić pozostałej części aplikacji kontynuować normalne działanie, chyba że z jakiegoś powodu próba załadowania dokumentu spowodowała uszkodzenie stanu innej rzeczy w systemie .

Zasadniczo, właściwa obsługa wyjątku często zależy w mniejszym stopniu od typu wyjątku niż miejsca, w którym został zgłoszony; jeśli zasób jest chroniony przez blokadę do odczytu i zapisu, a w metodzie, która uzyskała blokadę do odczytu, zgłaszany jest wyjątek, właściwe zachowanie powinno zasadniczo polegać na zwolnieniu blokady, ponieważ metoda nie mogła nic zrobić z zasobem . Jeśli wyjątek zostanie zgłoszony podczas uzyskiwania blokady do zapisu, blokada powinna często być unieważniana, ponieważ strzeżony zasób może być w niepoprawnym stanie; jeśli operacja podstawowa blokowania nie ma stanu „unieważnionego”, należy dodać flagę, aby śledzić takie unieważnienie. Zwolnienie blokady bez unieważnienia jest złe, ponieważ inny kod może widzieć chroniony obiekt w nieprawidłowym stanie. Pozostawienie zwisającego zamka nie jest jednak albo właściwe rozwiązanie. Właściwym rozwiązaniem jest unieważnienie blokady, aby wszelkie oczekujące lub przyszłe próby przejęcia natychmiast zakończyły się niepowodzeniem.

Jeśli okaże się, że nieważny zasób zostanie porzucony przed próbą jego użycia, nie ma powodu, aby miał on wyłączyć aplikację. Jeśli unieważniony zasób ma krytyczne znaczenie dla kontynuacji działania aplikacji, aplikacja będzie musiała zostać wyłączona, ale unieważnienie zasobu prawdopodobnie tak się stanie. Kod, który otrzymał pierwotny wyjątek, często nie ma możliwości dowiedzenia się, która sytuacja ma zastosowanie, ale jeśli unieważni on zasób, może zapewnić, że w obu przypadkach zostanie wykonane prawidłowe działanie.

supercat
źródło
2
To nie odpowiada na pytanie, ponieważ obsługa wyjątku bez znajomości szczegółów wyjątku! = Dyskretne wykorzystanie (ogólnego) wyjątku.
Taemyr
@Taemyr: Jeśli celem funkcji jest „wykonać X, jeśli to możliwe, albo wskazać, że nie było”, a X nie było możliwe, często niepraktyczne jest wymienienie wszelkiego rodzaju wyjątków, których żadna funkcja dzwoniącemu nie zależy też na tym, że „nie można było wykonać X”. Obsługa wyjątków od pokemonów jest często jedynym praktycznym sposobem na sprawienie, aby rzeczy działały w takich przypadkach i nie muszą być tak złe, jak niektórzy zauważają, jeśli kod dyscyplinuje się w unieważnianiu rzeczy, które ulegają uszkodzeniu w wyniku nieoczekiwanych wyjątków.
supercat
Dokładnie. Każda metoda powinna mieć cel, jeśli może wypełnić swój cel niezależnie od tego, czy jakakolwiek metoda, którą wywołuje, zgłasza wyjątek, czy nie, wtedy właściwe jest przełknięcie wyjątku, cokolwiek to jest, i wykonanie swojej pracy. Obsługa błędów polega zasadniczo na wykonaniu zadania po błędzie. To się liczy, a nie błąd.
jmoreno
@jmoreno: Trudne jest to, że w wielu sytuacjach będzie wiele wyjątków, z których wiele nie spodziewałby się szczególnie programista, co nie oznaczałoby problemu, i kilka wyjątków, z których niektóre nie szczególnie przewidują, które wskazują na problem, i nie ma określonego systematycznego sposobu ich rozróżnienia. Jedynym znanym mi sposobem na to, by sobie z tym poradzić, jest wyjątki, które unieważniają rzeczy, które zostały zepsute, więc jeśli mają znaczenie, zostaną zauważeni, a jeśli nie mają znaczenia, zostaną zignorowani.
supercat
-2

Połknięcie tego rodzaju błędu nie jest szczególnie przydatne dla nikogo. Tak, oryginalny rozmówca może nie dbają o wyjątku ale ktoś inny potęgi .

Więc co oni robią? Dodają kod do obsługi wyjątku, aby zobaczyć, jaki smak wyjątku otrzymają. Świetny. Ale jeśli zostanie pozostawiony moduł obsługi wyjątków, oryginalny obiekt wywołujący nie będzie już miał wartości zerowej i coś się zepsuje w innym miejscu.

Nawet jeśli zdajesz sobie sprawę, że jakiś kod nadrzędny może zgłosić błąd, a tym samym zwrócić wartość null, byłoby poważnie obojętne, abyś przynajmniej nie próbował wyłączyć wyjątku w kodzie wywołującym IMHO.

Robbie Dee
źródło
„poważnie bezwładny” - czy może miałeś na myśli „poważnie nieudolny”?
Nazwa ezoteryczna
@EsotericScreenName Wybierz jeden ... ;-)
Robbie Dee
-3

Widziałem przykłady, w których biblioteki stron trzecich miały potencjalnie przydatne metody, z wyjątkiem tego, że wprowadzają wyjątki w niektórych przypadkach, które powinny działać dla mnie. Co mogę zrobić?

  • Wdrażaj dokładnie to, czego potrzebuję. Może być trudny w terminie.
  • Zmodyfikuj kod innej firmy. Ale potem muszę szukać konfliktów scalania przy każdym uaktualnieniu.
  • Napisz metodę otoki, aby przekształcić wyjątek w zwykły wskaźnik zerowy, boolean false lub cokolwiek innego.

Na przykład metoda biblioteczna

public foo findFoo() {...} 

zwraca pierwsze foo, ale jeśli nie ma, zgłasza wyjątek. Ale potrzebuję metody zwrotu pierwszego foo lub wartości zerowej. Więc piszę ten:

public foo myFindFoo() {
    try {
        return findFoo() 
    } 
    catch (NoFooException ex) {
        return null;
    }
}

Nie schludnie. Ale czasami pragmatyczne rozwiązanie.

om
źródło
1
Co masz na myśli mówiąc „zgłaszaj wyjątki tam, gdzie powinny one dla Ciebie działać”?
corsiKa
@ corsiKa, przykładem, który przychodzi mi na myśl, są metody przeszukiwania listy lub podobnej struktury danych. Czy zawodzą, gdy nic nie znajdują, czy zwracają wartość zerową? Innym przykładem są metody waitUntil, które kończą się niepowodzeniem po przekroczeniu limitu czasu zamiast zwracania logicznej wartości false.
om