Powiedziano mi, że wyjątków należy używać tylko w wyjątkowych przypadkach. Skąd mam wiedzieć, czy moja sprawa jest wyjątkowa?

99

Moim szczególnym przypadkiem jest to, że użytkownik może przekazać ciąg znaków do aplikacji, aplikacja analizuje go i przypisuje do obiektów strukturalnych. Czasami użytkownik może wpisać coś nieprawidłowego. Na przykład ich wkład może opisywać osobę, ale może powiedzieć, że jej wiek to „jabłko”. Prawidłowe zachowanie w takim przypadku polega na wycofaniu transakcji i poinformowaniu użytkownika o wystąpieniu błędu, a on będzie musiał spróbować ponownie. Może istnieć wymóg zgłaszania każdego błędu, jaki znajdziemy w danych wejściowych, a nie tylko pierwszego.

W tym przypadku argumentowałem, że powinniśmy zgłosić wyjątek. Nie zgodził się, mówiąc: „Wyjątki powinny być wyjątkowe: oczekuje się, że użytkownik może wprowadzić nieprawidłowe dane, więc nie jest to wyjątkowy przypadek”. Naprawdę nie wiedziałem, jak argumentować ten punkt, ponieważ z definicji tego słowa on wydaje się mieć rację.

Ale rozumiem, że właśnie dlatego Wyjątki zostały wymyślone w pierwszej kolejności. Kiedyś trzeba było sprawdzić wynik, aby zobaczyć, czy wystąpił błąd. Jeśli nie sprawdzisz, złe rzeczy mogą się zdarzyć bez Twojej uwagi.

Bez wyjątków każdy poziom stosu musi sprawdzić wynik wywoływanych metod, a jeśli programista zapomni sprawdzić na jednym z tych poziomów, kod może przypadkowo kontynuować i zapisać nieprawidłowe dane (na przykład). W ten sposób wydaje się bardziej podatny na błędy.

W każdym razie nie krępuj się i popraw coś, co tu powiedziałem. Moje główne pytanie brzmi: czy ktoś mówi, że wyjątki powinny być wyjątkowe, skąd mam wiedzieć, czy moja sprawa jest wyjątkowa?

Daniel Kaplan
źródło
3
możliwy duplikat? Kiedy wyjątek . Chociaż tam było zamknięte, ale myślę, że tu pasuje. To wciąż trochę filozofii, niektórzy ludzie i społeczności postrzegają wyjątki jako rodzaj kontroli przepływu.
thorsten müller
8
Gdy użytkownicy są głupi, zapewniają nieprawidłowe dane wejściowe. Gdy użytkownicy są sprytni, grają, podając nieprawidłowe dane wejściowe. Dlatego nieprawidłowe dane wprowadzone przez użytkownika nie są wyjątkiem.
mouviciel
7
Nie należy również mylić wyjątku , który jest bardzo specyficznym mechanizmem w Javie i .NET, z błędem, który jest terminem znacznie bardziej ogólnym. Obsługa błędów polega nie tylko na zgłaszaniu wyjątków. Ta dyskusja dotyczy niuansów między wyjątkami a błędami .
Eric King
4
„Wyjątkowy”! = „Rzadko się zdarza”
ConditionRacer
3
Uważam, że wyjątki Vexing Erica Lipperta są przyzwoitą radą.
Brian,

Odpowiedzi:

87

Wymyślono wyjątki, aby ułatwić obsługę błędów przy mniejszym bałaganie kodu. Powinieneś ich używać w przypadkach, gdy ułatwiają obsługę błędów przy mniejszym bałaganie kodu. Ta „wyjątki tylko w wyjątkowych okolicznościach” wywodzi się z czasów, gdy obsługa wyjątków została uznana za niedopuszczalny spadek wydajności. Tak już nie jest w przeważającej większości kodu, ale ludzie wciąż wyrzucają regułę, nie pamiętając przyczyny.

Zwłaszcza w Javie, która jest chyba najbardziej kochającym wyjątki językiem, jaki kiedykolwiek wymyślono, nie powinieneś czuć się źle, używając wyjątków, gdy upraszcza to twój kod. W rzeczywistości własna Integerklasa Java nie ma możliwości sprawdzenia, czy łańcuch jest prawidłową liczbą całkowitą bez potencjalnego wyrzucenia NumberFormatException.

Ponadto, chociaż nie możesz polegać tylko na sprawdzaniu poprawności interfejsu użytkownika, pamiętaj, że jeśli interfejs użytkownika jest poprawnie zaprojektowany, na przykład za pomocą pokrętła do wprowadzania krótkich wartości liczbowych, wówczas wartość nienumeryczna, która dostanie się do zaplecza, naprawdę byłaby wyjątkowy stan.

Karl Bielefeldt
źródło
10
Ładne machnięcie, tam. W rzeczywistości, w zaprojektowanej przeze mnie rzeczywistej aplikacji, hit wydajności zrobił różnicę i musiałem to zmienić, aby nie zgłaszać wyjątków dla niektórych operacji parsowania.
Robert Harvey
17
Nie twierdzę, że nadal nie ma przypadków, w których poprawa wydajności jest prawidłowym powodem, ale te przypadki są raczej wyjątkiem (gra słów zamierzona) niż regułą.
Karl Bielefeldt
11
@RobertHarvey Sztuką w Javie jest rzucanie gotowych obiektów wyjątków zamiast throw new .... Lub wyrzuć niestandardowe wyjątki, w których fillInStackTrace () jest zastępowany. Nie powinieneś wtedy zauważać spadku wydajności, nie mówiąc już o hitach .
Ingo
3
+1: Dokładnie to, co zamierzam odpowiedzieć. Użyj go, gdy uprości kod. Wyjątki mogą dać znacznie wyraźniejszy kod, w którym nie musisz zawracać sobie głowy sprawdzaniem wartości zwracanych na każdym poziomie stosu wywołań. (Podobnie jak wszystko inne, jeśli użyty w niewłaściwy sposób może sprawić, że Twój kod stanie się okropnym bałaganem).
Leo
4
@Brendan Załóżmy, że wystąpił wyjątek, a kod obsługi błędów znajduje się 4 poziomy poniżej w stosie wywołań. Jeśli używasz kodu błędu, wszystkie 4 funkcje powyżej modułu obsługi muszą mieć typ kodu błędu jako wartość zwracaną i musisz wykonać łańcuch if (foo() == ERROR) { return ERROR; } else { // continue }na każdym poziomie. Jeśli zgłosisz niezaznaczony wyjątek, nie będzie głośnego i zbędnego „błędu powrotu błędu”. Ponadto, jeśli przekazujesz funkcje jako argument, użycie kodu błędu może zmienić sygnaturę funkcji na niezgodny typ, nawet jeśli błąd może nie wystąpić.
Doval,
72

Kiedy należy wprowadzić wyjątek? Jeśli chodzi o kod, myślę, że następujące wyjaśnienie jest bardzo pomocne:

Wyjątkiem jest sytuacja, gdy członek nie wykona zadania, które powinien wykonać zgodnie ze swoją nazwą . (Jeffry Richter, CLR przez C #)

Dlaczego to jest pomocne? Sugeruje to, że zależy od kontekstu, kiedy coś należy traktować jako wyjątek, czy nie. Na poziomie wywołań metod kontekst podaje: (a) nazwa, (b) podpis metody i (b) kod klienta, który używa lub oczekuje się, że użyje metody.

Aby odpowiedzieć na twoje pytanie, powinieneś zajrzeć do kodu, w którym przetwarzane są dane wprowadzone przez użytkownika. Może to wyglądać mniej więcej tak:

public void Save(PersonData personData) {  }

Czy nazwa metody sugeruje wykonanie pewnej weryfikacji? Nie. W takim przypadku niepoprawny PersonData powinien zgłosić wyjątek.

Załóżmy, że klasa ma inną metodę, która wygląda następująco:

public ValidationResult Validate(PersonData personData) {  }

Czy nazwa metody sugeruje wykonanie pewnej weryfikacji? Tak. W takim przypadku niepoprawne dane osobowe nie powinny zgłaszać wyjątku.

Podsumowując, obie metody sugerują, że kod klienta powinien wyglądać następująco:

ValidationResult validationResult = personRegister.Validate(personData);
if (validationResult.IsValid())
{
    personRegister.Save(personData)
}
else
{
    // Throw an exception? To answer this look at the context!
    // That is: (a) Method name, (b) signature and
    // (c) where this method is (expected) to be used.
}

Jeśli nie jest jasne, czy metoda powinna zgłosić wyjątek, być może przyczyną jest źle wybrana nazwa lub podpis metody. Może projekt klasy nie jest jasny. Czasami trzeba zmodyfikować projekt kodu, aby uzyskać jasną odpowiedź na pytanie, czy należy zgłosić wyjątek, czy nie.

Theo Lenndorff
źródło
Wczoraj stworzyłem struct„ValidationResult” i uporządkowałem swój kod tak, jak to opisujesz.
Paul
4
Nie pomaga odpowiedzieć na twoje pytanie, ale chciałbym tylko zauważyć, że w sposób dorozumiany lub celowy zastosowałeś się do zasady rozdzielania zapytań ( en.wikipedia.org/wiki/Command-query_separation ). ;-)
Theo Lenndorff
Dobry pomysł! Jedna wada: w twoim przykładzie sprawdzanie poprawności jest faktycznie wykonywane dwukrotnie: raz podczas Validate(zwracanie wartości False, jeśli jest nieważne) i raz podczas Save(rzucanie konkretnego, dobrze udokumentowanego wyjątku, jeśli jest nieważny). Oczywiście wynik sprawdzania poprawności można zapisać w pamięci podręcznej wewnątrz obiektu, ale spowodowałoby to dodatkową złożoność, ponieważ wynik sprawdzania poprawności musiałby zostać unieważniony przy zmianach.
Heinzi
@Heinzi Zgadzam się. Można go zrefaktoryzować, aby Validate()wywoływał się w Save()metodzie, a konkretne szczegóły z tego ValidationResultmogłyby zostać wykorzystane do skonstruowania odpowiedniego komunikatu dla wyjątku.
Phil,
3
Myślę, że to lepsze niż zaakceptowana odpowiedź. Rzuć, gdy połączenie nie może zrobić tego, co powinno.
Andy,
31

Wyjątki powinny być wyjątkowe: oczekuje się, że użytkownik może wprowadzić nieprawidłowe dane, więc nie jest to wyjątkowy przypadek

W związku z tym argumentem:

  • Oczekuje się, że plik może nie istnieć, więc nie jest to wyjątkowy przypadek.
  • Oczekuje się, że połączenie z serwerem może zostać utracone, więc nie jest to wyjątkowy przypadek
  • Oczekuje się, że plik konfiguracyjny może być zniekształcony, więc nie jest to wyjątkowy przypadek
  • Oczekuje się, że twoja prośba może czasem wypaść, więc nie jest to wyjątkowy przypadek

Jakikolwiek wyjątek, który złapiesz, musisz się spodziewać, ponieważ, cóż, postanowiłeś go złapać. Dzięki tej logice nigdy nie powinieneś rzucać żadnych wyjątków, które faktycznie planujesz złapać.

Dlatego uważam, że „wyjątki powinny być wyjątkowe” to okropna zasada.

To, co powinieneś zrobić, zależy od języka. Różne języki mają różne konwencje dotyczące tego, kiedy należy zgłaszać wyjątki. Na przykład Python generuje wyjątki od wszystkiego, a gdy w Pythonie podążam za nimi. Z drugiej strony C ++ generuje stosunkowo niewiele wyjątków i tam podążam za nimi. Możesz traktować C ++ lub Javę jak Python i wyrzucać wyjątki od wszystkiego, ale pracujesz w sprzeczności z tym, jak ten język będzie używany.

Wolę podejście Pythona, ale uważam, że kiepskim pomysłem jest użycie innych języków.

Winston Ewert
źródło
1
@gnat, wiem. Chodzi mi o to, że powinieneś przestrzegać konwencji języka (w tym przypadku Java), nawet jeśli nie są one twoimi ulubionymi.
Winston Ewert
6
+1 "exceptions should be exceptional" is a terrible rule of thumb.Dobrze powiedziane! To jedna z tych rzeczy, które ludzie powtarzają, nie myśląc o nich.
Andres F.,
2
„Oczekiwany” jest definiowany nie przez subiektywny argument lub konwencję, ale przez kontrakt funkcji / funkcji API (może to być wyraźne, ale często tylko sugerowane). Różne funkcje / interfejsy API / podsystemy mogą mieć różne oczekiwania, np. W przypadku niektórych funkcji wyższego poziomu oczekiwany jest nieistniejący plik (może to zgłosić użytkownikowi za pośrednictwem GUI), w przypadku innych funkcji niższego poziomu prawdopodobnie nie (i dlatego powinien zgłosić wyjątek). Wydaje się, że w tej odpowiedzi brakuje ważnego punktu ...
mikera
1
@mikera, tak, funkcja powinna (tylko) wyrzucić wyjątki określone w jej umowie. Ale to nie jest pytanie. Pytanie brzmi, jak zdecydujesz, jaka powinna być ta umowa. Twierdzę, że ogólna zasada „wyjątki powinny być wyjątkowe” nie jest pomocna w podejmowaniu tej decyzji.
Winston Ewert
1
@ Supercat, nie sądzę, że to naprawdę ma znaczenie, co w końcu jest bardziej powszechne. Myślę, że krytycznym pytaniem jest rozsądna domyślność. Jeśli nie jawnie obsługuję warunek błędu, czy mój kod udaje, że nic się nie wydarzyło, czy pojawia się przydatny komunikat o błędzie?
Winston Ewert
30

Zawsze myślę o takich rzeczach, jak dostęp do serwera bazy danych lub interfejsu API sieci Web, gdy myślę o wyjątkach. Oczekujesz, że interfejs API serwera / sieci będzie działał, ale w wyjątkowych przypadkach może nie działać (serwer nie działa). Żądanie internetowe może zwykle być szybkie, ale w wyjątkowych okolicznościach (duże obciążenie) może upłynąć limit czasu. To jest coś poza twoją kontrolą.

Dane wejściowe użytkowników są pod twoją kontrolą, ponieważ możesz sprawdzić, co wysyłają i zrobić z nimi, co chcesz. W twoim przypadku sprawdziłbym dane wejściowe użytkownika, zanim spróbowałbym je zapisać. Zgadzam się, że użytkownicy, którzy podają nieprawidłowe dane, powinni się spodziewać, a Twoja aplikacja powinna to uwzględnić, sprawdzając poprawność danych wejściowych i wyświetlając przyjazny dla użytkownika komunikat o błędzie.

To powiedziawszy, używam wyjątków w większości moich ustawiaczy modeli domen, w których absolutnie nie powinno być szansy na wejście nieprawidłowych danych. Jest to jednak ostatnia linia obrony i mam tendencję do budowania formularzy wejściowych przy użyciu bogatych reguł sprawdzania poprawności , dzięki czemu praktycznie nie ma szans na wywołanie tego wyjątku modelu domeny. Kiedy więc rozgrywający oczekuje jednej rzeczy, a dostaje inną, jest to wyjątkowa sytuacja, która nie powinna mieć miejsca w zwykłych okolicznościach.

EDYCJA (coś jeszcze do rozważenia):

Wysyłając dane dostarczone przez użytkownika do bazy danych, wcześniej wiesz, co powinieneś i nie powinieneś wprowadzać do swoich tabel. Oznacza to, że dane mogą być sprawdzane pod kątem oczekiwanego formatu. To jest coś, co możesz kontrolować. Tym, czego nie możesz kontrolować, jest awaria serwera w trakcie zapytania. Więc wiesz, że zapytanie jest w porządku, a dane są filtrowane / sprawdzane, próbujesz zapytania i nadal się nie powiedzie, jest to wyjątkowa sytuacja.

Podobnie w przypadku żądań internetowych, nie możesz wiedzieć, czy upłynie limit czasu żądania, czy nie uda się nawiązać połączenia, zanim spróbujesz go wysłać. Gwarantuje to również podejście try / catch, ponieważ nie można zapytać serwera, czy zadziała kilka milisekund później, gdy wyślesz żądanie.

Ivan Pintar
źródło
8
Ale dlaczego? Dlaczego wyjątki są mniej przydatne w obsłudze bardziej oczekiwanych problemów?
Winston Ewert
6
@ Pinetree, sprawdzanie istnienia pliku przed otwarciem pliku jest złym pomysłem. Plik może przestać istnieć między sprawdzaniem a otwieraniem, plik nie może mieć uprawnień, które pozwolą go otworzyć, a sprawdzenie istnienia i następnie otwarcie pliku będzie wymagało dwóch kosztownych wywołań systemowych. Lepiej spróbuj otworzyć plik, a następnie poradzić sobie z niepowodzeniem.
Winston Ewert
10
O ile widzę, prawie wszystkie możliwe awarie lepiej sobie radzić, niż wyzdrowieć po awarii, niż próbować sprawdzić sukces przed rozdaniem. To, czy używasz wyjątków, czy czegoś innego wskazuje na niepowodzenie, jest osobnym problemem. Wolę wyjątki, ponieważ nie mogę ich przypadkowo zignorować.
Winston Ewert
11
Nie zgadzam się z twoim założeniem, że ponieważ spodziewane są nieprawidłowe dane użytkownika, nie można ich uznać za wyjątkowe. Jeśli piszę analizator składni, a ktoś podaje mu dane nie do porównania, jest to wyjątek. Nie mogę kontynuować analizy. Sposób obsługi wyjątku to zupełnie inne pytanie.
ConditionRacer
4
File.ReadAllBytes wyrzuci a, FileNotFoundExceptiongdy otrzyma nieprawidłowe dane wejściowe (na przykład nieistniejącą nazwę pliku). To jedyny prawidłowy sposób zagrożenia tym błędem. Co jeszcze można zrobić bez zwracania kodów błędów?
2013
16

Odniesienie

Od pragmatycznego programisty:

Uważamy, że wyjątki rzadko powinny być stosowane jako część normalnego przepływu programu; wyjątki powinny być zarezerwowane na nieoczekiwane zdarzenia. Załóżmy, że nieprzechwycony wyjątek spowoduje zakończenie działania programu i zadaj sobie pytanie: „Czy ten kod będzie nadal działał, jeśli usunę wszystkie programy obsługi wyjątków?” Jeśli odpowiedź brzmi „nie”, być może w wyjątkowych okolicznościach stosowane są wyjątki.

Następnie badają przykład otwierania pliku do odczytu, a plik nie istnieje - czy to powinno stanowić wyjątek?

Jeśli plik powinien tam być, to wyjątek jest gwarantowany. [...] Z drugiej strony, jeśli nie masz pojęcia, czy plik powinien istnieć, czy nie, to nie wydaje się wyjątkowy, jeśli nie możesz go znaleźć, a zwrot błędu jest odpowiedni.

Później dyskutują, dlaczego wybrali takie podejście:

[A] n wyjątek stanowi natychmiastowe, nielokalne przekazanie kontroli - jest to rodzaj kaskadowania goto. Programy korzystające z wyjątków w ramach normalnego przetwarzania mają wszystkie problemy z czytelnością i utrzymywalnością klasycznego kodu spaghetti. Programy te przerywają enkapsulację: procedury i ich osoby wywołujące są ściślej powiązane dzięki obsłudze wyjątków.

Jeśli chodzi o twoją sytuację

Twoje pytanie sprowadza się do pytania „Czy błędy weryfikacji powinny powodować wyjątki?” Odpowiedź jest taka, że ​​zależy to od tego, gdzie odbywa się walidacja.

Jeśli dana metoda znajduje się w sekcji kodu, w której zakłada się, że dane wejściowe zostały już zatwierdzone, nieprawidłowe dane wejściowe powinny zgłaszać wyjątek; jeśli kod jest zaprojektowany w taki sposób, że ta metoda otrzyma dokładne dane wprowadzone przez użytkownika, należy oczekiwać nieprawidłowych danych i nie należy zgłaszać wyjątku.

Mike Partridge
źródło
11

Jest tu wiele filozoficznej pontyfikacji, ale ogólnie mówiąc, wyjątkowymi warunkami są po prostu te warunki, których nie możesz lub nie chcesz (z wyjątkiem czyszczenia, raportowania błędów itp.) Bez interwencji użytkownika. Innymi słowy, są to warunki niemożliwe do odzyskania.

Jeśli podasz programowi ścieżkę pliku, z zamiarem przetworzenia tego pliku w jakiś sposób, a plik określony przez tę ścieżkę nie istnieje, jest to wyjątkowy warunek. Nie możesz nic na to poradzić w kodzie, poza zgłoszeniem tego użytkownikowi i zezwoleniem mu na określenie innej ścieżki pliku.

Robert Harvey
źródło
1
+1, bardzo blisko tego, co chciałem powiedzieć. Powiedziałbym, że bardziej dotyczy zakresu i naprawdę nie ma nic wspólnego z użytkownikiem. Dobrym przykładem tego jest różnica między dwiema funkcjami .Net int.Parse i int.TryParse, ta pierwsza nie ma innego wyjścia, jak tylko rzucić wyjątek na złe dane wejściowe, później nie powinna nigdy rzucać wyjątku
jmoreno 24.01.2013
1
@jmoreno: Ergo, użyłbyś TryParse, gdy kod mógłby zrobić coś z nieusuwalnym warunkiem, i Parsował, gdy nie mógł.
Robert Harvey
7

Należy wziąć pod uwagę dwa problemy:

  1. omawiasz jeden problem - nazwijmy go, Assignerponieważ ten problem dotyczy przypisywania danych wejściowych do obiektów strukturalnych - i wyrażasz ograniczenie, że dane wejściowe są poprawne

  2. dobrze wdrożony interfejs użytkownika posiada dodatkową troskę: walidacja danych wejściowych użytkownika i konstruktywnej informacji zwrotnej na temat błędów (nazwijmy to część Validator)

Z punktu widzenia Assignerkomponentu zgłoszenie wyjątku jest całkowicie uzasadnione, ponieważ wyrażono ograniczenie, które zostało naruszone.

Z punktu widzenia doświadczenia użytkownika użytkownik nie powinien rozmawiać z tym bezpośrednio Assigner. Powinny one być rozmowy z nim poprzezValidator .

Teraz, w przypadku Validatornieprawidłowego wprowadzania danych przez użytkownika, nie jest to wyjątkowy przypadek, tak naprawdę jest to przypadek, który Cię bardziej interesuje. Więc tutaj wyjątek nie byłby odpowiedni, i tutaj również chciałbyś zidentyfikować wszystkie błędy zamiast ratować za pierwszym razem.

Zauważysz, że nie wspomniałem o tym, jak te obawy są realizowane. Wygląda na to, że mówisz o tym, Assignera twój kolega mówi o połączeniu Validator+Assigner. Kiedy uświadomisz sobie, że istnieją dwie odrębne (lub możliwe do rozdzielenia) obawy, przynajmniej możesz je rozsądnie przedyskutować.


Aby rozwiązać komentarz Renana, ja tylko przy założeniu , że po zidentyfikowaniu swoje dwa oddzielne obawy, to oczywiste, co należy uznać za przypadki wyjątkowe w każdym kontekście.

W rzeczywistości, jeśli nie jest oczywiste, czy coś należy uznać za wyjątkowe, twierdzę, że prawdopodobnie nie skończyłeś identyfikować niezależnych problemów w swoim rozwiązaniu.

To chyba bezpośrednia odpowiedź

... skąd mam wiedzieć, czy moja sprawa jest wyjątkowa?

upraszczaj, aż będzie to oczywiste . Kiedy masz stos prostych pojęć, które dobrze rozumiesz, możesz jasno uzasadnić ich ponowne złożenie w kod, klasy, biblioteki lub cokolwiek innego.

Nieprzydatny
źródło
-1 Tak, istnieją dwie obawy, ale to nie odpowiada na pytanie „Skąd mam wiedzieć, czy moja sprawa jest wyjątkowa?”
RMalke
Chodzi o to, że ten sam przypadek może być wyjątkowy w jednym kontekście, a nie w innym. Określenie kontekstu, o którym naprawdę mówisz (zamiast łączenia ich obu), odpowiada na pytanie tutaj.
Bezużyteczne
... właściwie może tak nie jest - zamiast tego zająłem się twoim punktem.
Bezużyteczne
4

Inni dobrze odpowiedzieli, ale wciąż tu jest moja krótka odpowiedź. Wyjątkiem jest sytuacja, w której coś w środowisku ma coś złego, czego nie można kontrolować, a kod nie może w ogóle iść do przodu. W takim przypadku musisz również poinformować użytkownika, co poszło źle, dlaczego nie możesz iść dalej i jaka jest rozdzielczość.

Manoj R.
źródło
3

Nigdy nie byłem wielkim fanem rady, że powinieneś rzucać wyjątki tylko w wyjątkowych przypadkach, częściowo dlatego, że nic nie mówi (to tak, jakby powiedzieć, że powinieneś jeść tylko jedzenie, które jest jadalne), ale także dlatego, że jest bardzo subiektywna i często nie jest jasne, co stanowi wyjątkowy przypadek, a co nie.

Istnieją jednak dobre powody, dla których ta rada: wyrzucanie i wychwytywanie wyjątków jest powolne, a jeśli uruchamiasz swój kod w debuggerze w Visual Studio z ustawieniem powiadamiania cię za każdym razem, gdy zostanie zgłoszony wyjątek, możesz zostać spamowany przez dziesiątki jeśli nie setki wiadomości na długo przed wystąpieniem problemu.

Zasadniczo, jeśli:

  • twój kod jest wolny od błędów i
  • usługi, od których zależy, są dostępne, oraz
  • Twój użytkownik używa Twojego programu w sposób, w jaki miał być używany (nawet jeśli niektóre podane przez niego dane wejściowe są nieprawidłowe)

wtedy twój kod nigdy nie powinien wyrzucać wyjątku, nawet takiego, który zostanie przechwycony później. Aby przechwycić nieprawidłowe dane, możesz użyć walidatorów na poziomie interfejsu użytkownika lub kodu, na przykład Int32.TryParse()w warstwie prezentacji.

W każdym innym przypadku powinieneś trzymać się zasady, że wyjątek oznacza, że ​​twoja metoda nie może robić tego, co mówi jej nazwa. Zasadniczo nie jest dobrym pomysłem stosowanie kodów powrotu w celu wskazania niepowodzenia (chyba że nazwa metody wyraźnie wskazuje, że tak się dzieje, np. TryParse()) Z dwóch powodów. Po pierwsze, domyślną odpowiedzią na kod błędu jest zignorowanie warunku błędu i kontynuowanie niezależnie; po drugie, możesz zbyt łatwo skończyć z niektórymi metodami używającymi kodów powrotu i innymi metodami wykorzystującymi wyjątki i zapominając, który jest który. Widziałem nawet bazy kodu, w których dwie różne wymienne implementacje tego samego interfejsu przyjmują tutaj różne podejścia.

jammycakes
źródło
2

Wyjątki powinny reprezentować warunki, które prawdopodobnie nie zostaną przygotowane do obsługi kodu natychmiastowego wywołania, nawet jeśli metoda wywołująca mogłaby to zrobić. Rozważmy na przykład kod, który odczytuje niektóre dane z pliku, może słusznie założyć, że każdy prawidłowy plik zakończy się prawidłowym rekordem i nie jest wymagane wyodrębnienie żadnych informacji z rekordu częściowego.

Jeśli procedura odczytu danych nie używa wyjątków, ale po prostu informuje, czy odczyt się powiódł, kod wywołujący musiałby wyglądać następująco:

temp = dataSource.readInteger();
if (temp == null) return null;
field1 = (int)temp;
temp = dataSource.readInteger();
if (temp == null) return null;
field2 = (int)temp;
temp = dataSource.readString();
if (temp == null) return null;
field3 = temp;

itd. wydając trzy wiersze kodu na każdy użyteczny kawałek pracy. Natomiast jeśli readIntegerzgłosi wyjątek po napotkaniu końca pliku, a jeśli program wywołujący może po prostu przekazać wyjątek, kod staje się:

field1 = dataSource.readInteger();
field2 = dataSource.readInteger();
field3 = dataSource.readString();

O wiele prostszy i czystszy wygląd, ze znacznie większym naciskiem na przypadek, w którym wszystko działa normalnie. Zauważ, że w przypadkach, w których natychmiastowe rozmówca byłoby oczekiwać, aby obsłużyć warunek, metodę, która zwraca kod błędu często będzie bardziej pomocny niż ten, który zgłasza wyjątek. Na przykład, aby zsumować wszystkie liczby całkowite w pliku:

do
{
  temp = dataSource.tryReadInteger();
  if (temp == null) break;
  total += (int)temp;
} while(true);

przeciw

try
{
  do
  {
    total += (int)dataSource.readInteger();
  }
  while(true);
}
catch endOfDataSourceException ex
{ // Don't do anything, since this is an expected condition (eventually)
}

Kod pytający o liczby całkowite oczekuje, że jedno z tych wywołań zakończy się niepowodzeniem. Posługiwanie się kodem przez niekończącą się pętlę, która będzie działać, dopóki tak się nie stanie, jest o wiele mniej eleganckie niż użycie metody, która wskazuje awarie za pomocą wartości zwracanej.

Ponieważ klasy często nie wiedzą, jakich warunków będą lub nie będą oczekiwać ich klienci, często pomocne jest zaoferowanie dwóch wersji metod, które mogą zawieść w sposób, jakiego oczekują niektórzy rozmówcy, a inni nie. Takie postępowanie pozwoli na czyste stosowanie takich metod w obu typach rozmówców. Należy również pamiętać, że nawet metody „wypróbowania” powinny generować wyjątki, jeśli wystąpią sytuacje, że osoba dzwoniąca prawdopodobnie nie oczekuje. Na przykład tryReadIntegernie powinien zgłaszać wyjątku, jeśli napotka czysty warunek końca pliku (jeśli program wywołujący nie spodziewał się tego, użyłbyreadInteger). Z drugiej strony prawdopodobnie powinien zgłosić wyjątek, jeśli danych nie można odczytać, ponieważ np. Karta pamięci zawierająca je została odłączona. Chociaż takie zdarzenia powinny zawsze być rozpoznawane jako możliwość, jest mało prawdopodobne, aby kod wywoływania natychmiastowego był przygotowany na zrobienie czegokolwiek przydatnego w odpowiedzi; z pewnością nie powinien być zgłaszany w taki sam sposób, jak warunek końca pliku.

supercat
źródło
2

Najważniejszą rzeczą w pisaniu oprogramowania jest uczynienie go czytelnym. Wszystkie inne kwestie są drugorzędne, w tym uczynienie go wydajnym i poprawnym. Jeśli jest czytelny, resztę można załatwić podczas konserwacji, a jeśli nie jest czytelny, lepiej po prostu go wyrzuć. Dlatego należy zgłaszać wyjątki, gdy poprawia to czytelność.

Kiedy piszesz jakiś algorytm, pomyśl tylko o osobie, która będzie go czytać w przyszłości. Gdy dojdziesz do miejsca, w którym mógłby tam być potencjalny problem, należy zadać sobie pytanie, czy czytelnik chce zobaczyć, w jaki sposób poradzić sobie z tym problemem teraz , czy też czytelnik wolą po prostu dostać się na algorytmie?

Lubię wymyślić przepis na ciasto czekoladowe. Kiedy mówi ci, aby dodać jajka, ma wybór: może albo założyć, że masz jajka i przejść do przepisu, albo może rozpocząć wyjaśnienie, w jaki sposób możesz zdobyć jajka, jeśli nie masz jaj. Może wypełnić całą książkę technikami polowania na dzikie kurczaki, wszystko po to, aby upiec ciasto. To dobrze, ale większość ludzi nie będzie chciała czytać tego przepisu. Większość ludzi wolałaby po prostu założyć, że jajka są dostępne i zacząć od przepisu. To wezwanie do osądu, które autorzy muszą sporządzić, pisząc przepisy.

Nie ma żadnych gwarantowanych reguł dotyczących tego, co stanowi dobry wyjątek i problemów, które należy natychmiast rozwiązać, ponieważ wymaga to przeczytania w myślach czytelnika. Najlepsze, co kiedykolwiek zrobisz, to zasady ogólne, a „wyjątki dotyczą tylko wyjątkowych okoliczności” jest całkiem niezłe. Zwykle, gdy czytelnik czyta twoją metodę, szuka tego, co zrobi metoda w 99% przypadków, i wolałby, aby nie było zaśmiecone dziwacznymi przypadkami narożnymi, takimi jak kontaktowanie się z użytkownikami wprowadzającymi nielegalne dane i inne rzeczy, które prawie nigdy się nie zdarzają. Chcą zobaczyć normalny przepływ oprogramowania ułożony bezpośrednio, jedna po drugiej, tak jakby problemy nigdy się nie zdarzały.

Geo
źródło
2

Może istnieć wymóg zgłaszania każdego błędu, jaki znajdziemy w danych wejściowych, a nie tylko pierwszego.

Dlatego nie możesz tutaj zgodzić się na wyjątek. Wyjątek natychmiast przerywa proces sprawdzania poprawności. Więc byłoby wiele do obejścia, aby to zrobić.

Zły przykład:

Metoda sprawdzania poprawności dla Dogklasy z wykorzystaniem wyjątków:

void validate(Set<DogValidationException> previousExceptions) {
    if (!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        DogValidationException disallowedName = new DogValidationException(Problem.DISALLOWED_DOG_NAME);
        if (!previousExceptions.contains(disallowedName)){
            throw disallowedName;
        }
    }
    if (this.legs < 4) {
        DogValidationException invalidDog = new DogValidationException(Problem.LITERALLY_INVALID_DOG);
        if (!previousExceptions.contains(invalidDog)){
            throw invalidDog;
        }
    }
    // etc.
}

Jak to nazwać:

Set<DogValidationException> exceptions = new HashSet<DogValidationException>();
boolean retry;
do {
    retry = false;
    try {
        dog.validate(exceptions);
    } catch (DogValidationException e) {
        exceptions.add(e);
        retry = true;
    }
} while (retry);

if(exceptions.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

Problem polega na tym, że proces sprawdzania poprawności, aby uzyskać wszystkie błędy, wymagałby pominięcia już znalezionych wyjątków. Powyższe może zadziałać, ale jest to wyraźne niewłaściwe użycie wyjątków . Rodzaj weryfikacji, o który zostałeś poproszony, powinien nastąpić przed dotknięciem bazy danych. Nie trzeba więc niczego wycofywać. I wyniki walidacji prawdopodobnie będą błędami walidacji (ale mam nadzieję, że zero).

Lepszym podejściem jest:

Wywołanie metody:

Set<Problem> validationResults = dog.validate();
if(validationResults.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

Metoda walidacji:

Set<Problem> validate() {
    Set<Problem> result = new HashSet<Problem>();
    if(!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        result.add(Problem.DISALLOWED_DOG_NAME);
    }
    if(this.legs < 4) {
        result.add(Problem.LITERALLY_INVALID_DOG);
    }
    // etc.
    return result;
}

Dlaczego? Istnieje wiele powodów, a najwięcej powodów wskazano w innych odpowiedziach. Mówiąc prosto: inni czytają i rozumieją o wiele łatwiej . Po drugie, czy chcesz pokazać ślady stosu użytkowników, aby wytłumaczyć mu, że dogźle skonfigurował ?

Jeśli podczas zatwierdzania w drugim przykładzie nadal pojawia się błąd , nawet pomimo tego, że twój walidator sprawdził dogzerowe problemy, wówczas zgłoszenie wyjątku jest właściwe . Podobnie jak: Brak połączenia z bazą danych, wpis bazy danych został w międzyczasie zmodyfikowany przez kogoś innego.

Matthias Ronge
źródło