Sprawa przeciwko sprawdzonym wyjątkom

456

Od wielu lat nie jestem w stanie uzyskać porządnej odpowiedzi na następujące pytanie: dlaczego niektórzy programiści tak sprawdzają wyjątki? Przeprowadziłem wiele rozmów, czytałem rzeczy na blogach, czytałem, co powiedział Bruce Eckel (pierwsza osoba, którą widziałem, wypowiadała się przeciwko nim).

Obecnie piszę nowy kod i zwracam szczególną uwagę na sposób postępowania z wyjątkami. Próbuję zobaczyć punkt widzenia tłumu „nie lubimy sprawdzonych wyjątków” i nadal go nie widzę.

Każda rozmowa kończy się tym samym pytaniem, na które nie ma odpowiedzi ... pozwól, że skonfiguruję:

Ogólnie (na podstawie projektu Java),

  • Error dotyczy rzeczy, które nigdy nie powinny zostać złapane (VM ma alergię na orzeszki ziemne i ktoś upuścił na nią słoik orzeszków ziemnych)
  • RuntimeException dotyczy rzeczy, które programista zrobił źle (programista zszedł z końca tablicy)
  • Exception (z wyjątkiem RuntimeException ) dotyczy rzeczy, które są poza kontrolą programisty (dysk zapełnia się podczas zapisu do systemu plików, limit uchwytu pliku dla procesu został osiągnięty i nie można otworzyć więcej plików)
  • Throwable jest po prostu rodzicem wszystkich typów wyjątków.

Często słyszę argument, że jeśli zdarzy się wyjątek, to programista zamierza tylko wyjść z programu.

Innym częstym argumentem, jaki słyszę, jest to, że sprawdzone wyjątki utrudniają refaktoryzację kodu.

W przypadku argumentu „wszystko, co zamierzam zrobić, to wyjść” mówię, że nawet jeśli wychodzisz, musisz wyświetlić rozsądny komunikat o błędzie. Jeśli po prostu zastanawiasz się nad obsługą błędów, użytkownicy nie będą zbytnio zadowoleni, gdy program zostanie zamknięty bez wyraźnego wskazania przyczyny.

Dla tłumu „utrudnia refaktoryzację” oznacza to, że nie wybrano właściwego poziomu abstrakcji. Zamiast deklarować, że metoda zgłasza an IOException, IOExceptionpowinna zostać przekształcona w wyjątek, który jest bardziej odpowiedni dla tego, co się dzieje.

Nie mam problemu z opakowaniem Main za pomocą catch(Exception)(lub w niektórych przypadkach, catch(Throwable)aby program mógł wyjść z gracją z wdziękiem - ale zawsze wychwytuję określone wyjątki, których potrzebuję. W ten sposób mogę przynajmniej wyświetlić odpowiedni Komunikat o błędzie.

Pytanie, na które ludzie nigdy nie odpowiadają, brzmi:

Jeśli rzucasz RuntimeException podklasy zamiast Exception podklas, to skąd wiesz, co masz złapać?

Jeśli odpowiedź jest złapana, Exceptionmasz również do czynienia z błędami programisty w taki sam sposób jak wyjątki systemowe. Wydaje mi się to złe.

Jeśli złapiesz Throwable, traktujesz wyjątki systemowe i błędy maszyn wirtualnych (i tym podobne) w ten sam sposób. Wydaje mi się to złe.

Jeśli odpowiedź brzmi, że wychwytujesz tylko wyjątki, o których wiesz, że są zgłaszane, to skąd wiesz, jakie są zgłaszane? Co się stanie, gdy programista X zgłasza nowy wyjątek i zapomniał go złapać? To wydaje mi się bardzo niebezpieczne.

Powiedziałbym, że program, który wyświetla ślad stosu, jest zły. Czy ludzie, którzy nie lubią sprawdzonych wyjątków, nie czują się w ten sposób?

Jeśli więc nie lubisz sprawdzonych wyjątków, czy możesz wyjaśnić, dlaczego NIE ORAZ odpowiedzieć na pytanie, na które nie ma odpowiedzi, proszę?

Edycja: Nie szukam porady, kiedy użyć któregoś z modeli, szukam tego, dlaczego ludzie się przedłużają, RuntimeExceptionponieważ nie lubią przedłużać się Exceptioni / lub dlaczego wychwytują wyjątek, a następnie ponownie RuntimeExceptionrzucają, a nie dodają rzuty do ich metoda. Chcę zrozumieć motywację, by nie lubić sprawdzonych wyjątków.

TofuBeer
źródło
46
Nie sądzę, że jest to całkowicie subiektywne - jest to funkcja językowa, która została zaprojektowana z myślą o konkretnym zastosowaniu, a nie po to, aby każdy sam decydował, co to jest dla siebie. I nie jest to szczególnie kłótliwe, z góry odnosi się do konkretnych obaleń, które ludzie mogliby łatwo wymyślić.
Gareth,
6
Daj spokój. Ten temat, postrzegany jako funkcja języka, był i można do niego podejść w obiektywny sposób.
Kurt Schelfthout
6
@cletus „odpowiadając na twoje pytanie” gdybym miał odpowiedź, nie zadałbym tego pytania!
TofuBeer
8
Świetne pytanie. W C ++ nie ma żadnych sprawdzonych wyjątków i moim zdaniem funkcja wyjątku jest bezużyteczna. Sytuacja kończy się sytuacją, w której musisz złapać kontrolę nad każdym wykonanym wywołaniem funkcji, ponieważ po prostu nie wiesz, czy może to coś rzucić.
Dimitri C.
4
Najsilniejszym argumentem, jaki znam dla sprawdzonych wyjątków, jest to, że początkowo nie istniały one w Javie i że po ich wprowadzeniu odkryły setki błędów w JDK. Jest to nieco wcześniejsze niż Java 1.0. Osobiście nie byłbym bez nich i nie zgadzam się gwałtownie z Brucem Eckelem i innymi w tej sprawie.
Markiz Lorne,

Odpowiedzi:

276

Myślę, że przeczytałem ten sam wywiad z Brucem Eckelem, który przeprowadziłeś - i zawsze mnie to denerwowało. W rzeczywistości argument został wysunięty przez rozmówcę (jeśli rzeczywiście jest to post, o którym mówisz) Anders Hejlsberg, genialny MS za .NET i C #.

http://www.artima.com/intv/handcuffs.html

Choć jestem fanem Hejlsberga i jego twórczości, ten argument zawsze wydawał mi się fałszywy. Zasadniczo sprowadza się do:

„Sprawdzone wyjątki są złe, ponieważ programiści po prostu nadużywają ich, zawsze je łapiąc i odrzucając, co prowadzi do ukrywania i ignorowania problemów, które w innym przypadku zostałyby przedstawione użytkownikowi”.

Przez „inaczej przedstawione użytkownikowi” mam na myśli to, że jeśli użyjesz wyjątku środowiska wykonawczego, leniwy programista po prostu go zignoruje (zamiast złapać go z pustym blokiem catch) i użytkownik go zobaczy.

Podsumowanie argumentu jest takie, że „programiści nie będą ich używać poprawnie, a ich niewłaściwe użycie jest gorsze niż ich brak” .

Argument ten zawiera pewną prawdę i podejrzewam, że motywacja Goslingów do nieprzekazywania operatorów w Javie wynika z podobnego argumentu - mylą programistę, ponieważ są często wykorzystywani.

Ale ostatecznie uważam, że jest to fałszywy argument Hejlsberga i być może post-hoc stworzony w celu wyjaśnienia braku, a nie przemyślana decyzja.

Twierdziłbym, że podczas gdy nadmierne wykorzystanie sprawdzonych wyjątków jest złą rzeczą i prowadzi do niechlujnej obsługi przez użytkowników, ale ich właściwe użycie pozwala programistom API dać ogromne korzyści programistom klienta API.

Teraz programista API musi uważać, aby nie rzucać sprawdzonymi wyjątkami w inne miejsce, w przeciwnym razie po prostu drażnią programistę klienta. Bardzo leniwy programista kliencki (Exception) {}ucieknie się do złapania, gdy Hejlsberg ostrzega, a wszelkie korzyści zostaną utracone i nastąpi piekło. Ale w niektórych okolicznościach dobry sprawdzony wyjątek po prostu nie zastąpi.

Dla mnie klasycznym przykładem jest interfejs API do otwierania plików. Każdy język programowania w historii języków (przynajmniej w systemach plików) ma gdzieś interfejs API, który pozwala otworzyć plik. I każdy programista kliencki korzystający z tego interfejsu API wie, że musi sobie poradzić ze sprawą, że plik, który próbują otworzyć, nie istnieje. Pozwólcie, że sformułuję to: Każdy programista klienta korzystający z tego interfejsu API powinien wiedzieć , że musi poradzić sobie z tą sprawą. I jest rub: czy programista API może pomóc im wiedzieć, że powinni sobie z tym poradzić, komentując samemu, czy też może nalegać aby klient zajął się tym.

W języku C idiom wygląda mniej więcej tak

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

gdzie fopenoznacza niepowodzenie, zwracając 0, a C (głupio) pozwala traktować 0 jako wartość logiczną i ... Zasadniczo uczysz się tego idiomu i wszystko w porządku. Ale co jeśli jesteś noobem i nie nauczyłeś się tego idiomu. Potem oczywiście zaczynasz

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

i uczyć się na własnej skórze.

Zauważ, że mówimy tu tylko o silnie typowanych językach: Istnieje jasne pojęcie o tym, czym jest interfejs API w silnie typowanym języku: To jest zestaw funkcji (metod) do użycia z jasno zdefiniowanym protokołem dla każdego z nich.

Ten jasno zdefiniowany protokół jest zazwyczaj definiowany przez sygnaturę metody. Tutaj fopen wymaga, abyś przekazał mu ciąg (lub znak * w przypadku C). Jeśli podasz coś innego, pojawi się błąd kompilacji. Nie przestrzegałeś protokołu - nie używasz poprawnie interfejsu API.

W niektórych (niejasnych) językach typ zwracany jest również częścią protokołu. Jeśli spróbujesz wywołać odpowiednik fopen()w niektórych językach bez przypisywania go do zmiennej, otrzymasz również błąd czasu kompilacji (możesz to zrobić tylko przy użyciu funkcji void).

Chodzi mi o to, że: w języku o typie statycznym programista API zachęca klienta do właściwego używania interfejsu API, uniemożliwiając kompilację kodu klienta, jeśli popełni oczywiste błędy.

(W dynamicznie pisanym języku, takim jak Ruby, możesz przekazać wszystko, na przykład float, jako nazwę pliku - i to się skompiluje. Po co męczyć użytkownika sprawdzonymi wyjątkami, jeśli nawet nie zamierzasz kontrolować argumentów metody. przedstawione tutaj argumenty dotyczą tylko języków o typie statycznym).

A co ze sprawdzonymi wyjątkami?

Oto jeden z interfejsów API Java, których można użyć do otwarcia pliku.

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

Widzisz ten haczyk? Oto podpis tej metody API:

public FileInputStream(String name)
                throws FileNotFoundException

Uwaga: FileNotFoundExceptionjest to sprawdzony wyjątek.

Programista API mówi ci: „Możesz użyć tego konstruktora, aby utworzyć nowy FileInputStream, ale Ty

a) musi przekazać nazwę pliku jako ciąg
b) musi zaakceptować możliwość, że pliku nie można znaleźć w czasie wykonywania „

I o to mi chodzi.

Kluczem jest w zasadzie to, co pytanie brzmi: „Rzeczy, które są poza kontrolą programisty”. Moja pierwsza myśl była taka, że ​​on / ona ma na myśli rzeczy, które są poza API kontrolą programistów . Ale tak naprawdę sprawdzone wyjątki, gdy są właściwie używane, powinny naprawdę dotyczyć rzeczy, które są poza kontrolą programisty klienta i programisty API. Myślę, że to klucz do nie nadużywania sprawdzonych wyjątków.

Myślę, że otwieranie plików dobrze ilustruje tę kwestię. Programista API wie, że możesz nadać im nazwę pliku, która okaże się nieistniejąca w momencie wywołania API i że nie będzie w stanie zwrócić ci tego, co chciałeś, ale będzie musiał zgłosić wyjątek. Wiedzą również, że zdarza się to dość regularnie i że programista klienta może oczekiwać, że nazwa pliku będzie poprawna w momencie, w którym napisał wywołanie, ale może być niepoprawny w czasie wykonywania z powodów niezależnych od nich.

Interfejs API wyraźnie to wyjaśnia: będą przypadki, w których ten plik nie będzie istniał w momencie, gdy do mnie zadzwonisz, a ty lepiej sobie z nim poradzić.

Byłoby to wyraźniejsze w przypadku kontr-przypadku. Wyobraź sobie, że piszę interfejs API tabeli. Mam gdzieś model tabeli z interfejsem API zawierającym tę metodę:

public RowData getRowData(int row) 

Teraz jako programista API wiem, że będą przypadki, w których niektórzy klienci przekażą wartość ujemną dla wiersza lub wartości wiersza poza tabelą. Mogę więc kusić się, aby rzucić sprawdzony wyjątek i zmusić klienta do zajęcia się nim:

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(Oczywiście nie nazwałbym tego „sprawdzonym”).

Jest to złe użycie sprawdzonych wyjątków. Kod klienta będzie pełen wywołań do pobierania danych wierszy, z których każde będzie musiało użyć try / catch i po co? Czy zamierzają zgłosić użytkownikowi, że szukano niewłaściwego wiersza? Prawdopodobnie nie - ponieważ niezależnie od interfejsu użytkownika otaczającego mój widok tabeli, nie powinien pozwolić użytkownikowi wejść w stan, w którym żądany jest nielegalny wiersz. Jest to więc błąd programisty klienta.

Programista API nadal może przewidzieć, że klient będzie kodować takie błędy i powinien obsłużyć je z wyjątkiem środowiska wykonawczego takiego jak IllegalArgumentException.

Z zaznaczonym wyjątkiem getRowDatajest to wyraźnie przypadek, który doprowadzi leniwego programistę Hejlsberga do dodania pustych połowów. Gdy tak się stanie, niedozwolone wartości wierszy nie będą oczywiste nawet dla debugującego testera lub dewelopera klienta, a raczej doprowadzą do błędów domieszkowych, których trudno jest wskazać źródło. Rakiety Arianne wybuchną po starcie.

Okej, więc jest problem: mówię, że sprawdzony wyjątek FileNotFoundExceptionjest nie tylko dobrą rzeczą, ale niezbędnym narzędziem w zestawie narzędzi programistów API do definiowania API w najbardziej użyteczny sposób dla programisty klienta. Jest CheckedInvalidRowNumberExceptionto jednak duża niedogodność, która prowadzi do złego programowania i należy jej unikać. Ale jak odróżnić.

Wydaje mi się, że nie jest to nauka ścisła i sądzę, że leży ona u podstaw i być może uzasadnia do pewnego stopnia argument Hejlsberga. Ale nie cieszę się, że tutaj wylewam dziecko z kąpielą, więc pozwólcie mi wyodrębnić kilka zasad, aby odróżnić sprawdzone wyjątki od złych:

  1. Poza kontrolą klienta lub Zamknięty kontra Otwarty:

    Zaznaczone wyjątki należy stosować tylko wtedy, gdy przypadek błędu wymyka się spod kontroli zarówno API, jak i programisty klienta. Ma to związek z tym, jak otwarty lub zamknięty jest system. W ograniczonym interfejsie użytkownika, w którym programista klienta ma kontrolę, powiedzmy, nad wszystkimi przyciskami, poleceniami klawiatury itp., Które dodają i usuwają wiersze z widoku tabeli (system zamknięty), jest to błąd programistyczny klienta, jeśli próbuje pobrać dane z nieistniejący rząd. W systemie operacyjnym opartym na plikach, w którym dowolna liczba użytkowników / aplikacji może dodawać i usuwać pliki (system otwarty), możliwe jest, że plik, o który prosi klient, został usunięty bez ich wiedzy, dlatego należy się spodziewać, że poradzi sobie z tym .

  2. Wszechobecność:

    Sprawdzone wyjątki nie powinny być używane w wywołaniach API często wykonywanych przez klienta. Przez często mam na myśli wiele miejsc w kodzie klienta - niezbyt często na czas. Więc kod klienta nie próbuje często otwierać tego samego pliku, ale mój widok tabeli jest RowDatawszędzie z różnych metod. W szczególności zamierzam pisać dużo kodu

    if (model.getRowData().getCell(0).isEmpty())

    i bolesne będzie za każdym razem owijanie w try / catch.

  3. Informowanie użytkownika:

    Sprawdzone wyjątki należy stosować w przypadkach, w których można sobie wyobrazić przydatny komunikat o błędzie wyświetlany użytkownikowi końcowemu. To jest „i co zrobisz, kiedy to się stanie?” pytanie, które zadałem powyżej. Odnosi się to również do punktu 1. Ponieważ możesz przewidzieć, że coś poza twoim systemem klienta-API może spowodować, że pliku nie będzie, możesz rozsądnie poinformować o tym użytkownika:

    "Error: could not find the file 'goodluckfindingthisfile'"

    Ponieważ twój nielegalny numer wiersza został spowodowany wewnętrznym błędem i nie z winy użytkownika, naprawdę nie możesz podać żadnych przydatnych informacji. Jeśli Twoja aplikacja nie przepuści wyjątków środowiska wykonawczego do konsoli, prawdopodobnie skończy się to brzydkim komunikatem:

    "Internal error occured: IllegalArgumentException in ...."

    Krótko mówiąc, jeśli nie uważasz, że programista klienta może wyjaśnić twój wyjątek w sposób, który pomaga użytkownikowi, prawdopodobnie nie powinieneś używać sprawdzonego wyjątku.

To są moje zasady. Nieco wymyślone i bez wątpienia będą wyjątki (proszę pomóż mi je ulepszyć, jeśli chcesz). Ale moim głównym argumentem jest to, że istnieją przypadki, w FileNotFoundExceptionktórych sprawdzany wyjątek jest równie ważny i użyteczny jako część umowy API, jak typy parametrów. Nie powinniśmy więc rezygnować z tego tylko dlatego, że jest niewłaściwie używane.

Przepraszam, nie chciałem robić tego tak długo i głupio. Zakończę dwiema sugestiami:

Odp .: Programiści API: oszczędnie używają sprawdzonych wyjątków, aby zachować ich użyteczność. W razie wątpliwości użyj niezaznaczonego wyjątku.

B: Programiści klienccy: we wczesnym etapie tworzenia nawiniętego wyjątku (google it). JDK 1.4 i nowsze zawierają w RuntimeExceptiontym celu konstruktora , ale możesz też łatwo stworzyć własny. Oto konstruktor:

public RuntimeException(Throwable cause)

Następnie przyzwyczaj się do tego, gdy musisz obsłużyć sprawdzony wyjątek i czujesz się leniwy (lub myślisz, że programista API nadgorliwie używał sprawdzonego wyjątku w pierwszej kolejności), nie połykaj wyjątku, zawiń go i wrzućcie go ponownie.

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

Umieść to w jednym z małych szablonów kodu IDE i używaj go, gdy czujesz się leniwy. W ten sposób, jeśli naprawdę chcesz obsłużyć sprawdzony wyjątek, będziesz zmuszony wrócić i rozwiązać problem po zobaczeniu problemu w czasie wykonywania. Ponieważ, wierzcie mi (i Andersowi Hejlsbergowi), nigdy nie wrócicie do tego TODO w swoim

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}
Rabarbar
źródło
110
Otwieranie plików jest tak naprawdę dobrym przykładem na całkowicie przeciwne do zamierzonych wyjątki. Ponieważ przez większość czasu kod otwierający plik NIE może zrobić nic użytecznego w odniesieniu do wyjątku - lepiej to zrobić kilka warstw w górę stosu wywołań. Zaznaczony wyjątek zmusza cię do zaśmiecania podpisów metod, abyś mógł zrobić to, co i tak byś zrobił - obsłuż wyjątek w miejscu, w którym ma to sens.
Michael Borgwardt,
115
@Michael: Zgoda może generalnie obsłużyć te IO wyjątki kilka poziomów wyższych - ale nie słyszę Cię wątpliwości, że jako klient API Państwo mają przewidywać nich. Z tego powodu uważam, że sprawdzone wyjątki są odpowiednie. Tak, będziesz musiał zadeklarować „rzucenia” na stos wywołań każdej metody, ale nie zgadzam się, że jest to bałagan. Jest to przydatne informacje deklaratywne w podpisach metod. Mówisz: moje metody niskiego poziomu mogą napotkać brakujący plik, ale zostawią ci to. Nie widzę nic do stracenia i tylko dobry, czysty kod do zdobycia
Rabarbar
23
@ Rabarbar: +1, bardzo interesująca odpowiedź, pokazuje argumenty dla obu stron. Świetny. O twoim ostatnim komentarzu: pamiętaj, że deklarowanie rzucania się każdą metodą w górę stosu wywołań może nie zawsze być możliwe, szczególnie jeśli implementujesz interfejs po drodze.
R. Martinho Fernandes
8
Jest to bardzo silny argument (i ogólnie się z nim zgadzam), ale jest przypadek, w którym to nie działa: ogólne wywołania zwrotne. Jeśli mam bibliotekę wywołującą kod zdefiniowany przez użytkownika, nie ma możliwości (o której wiem, że w Javie), aby ta biblioteka propagowała sprawdzone wyjątki od kodu użytkownika, który wywołuje do kodu użytkownika, który go wywołuje. Ten problem dotyczy także innych części systemu typów wielu języków o typie statycznym, które nie obsługują odpowiednich typów ogólnych. Ta odpowiedź zostałaby poprawiona przez wzmiankę o tym (i być może odpowiedź).
Mankarse,
7
Źle @ Rabarbar. Sprawdzone wyjątki były przeznaczone na „nieprzewidziane sytuacje”, które można i należy traktować, ale zawsze były one niezgodne z najlepszą praktyką „rzucaj wcześnie, łap późno”. Otwieranie pliku to przykładowy przykład, z którym można sobie poradzić. Większość IOException (np. Zapis bajtu), SQLException, RemoteException jest niemożliwa do opanowania w znaczący sposób. Błąd lub ponowna próba powinny odbywać się na poziomie „biznesowym” lub „żądania”, a sprawdzone wyjątki - stosowane w bibliotekach Java - są błędem, który utrudnia to. literatejava.com/exceptions/…
Thomas W
184

Sprawdzone wyjątki polegają na tym, że tak naprawdę nie są wyjątkami w zwykłym rozumieniu tego pojęcia. Zamiast tego są alternatywnymi wartościami zwracanymi przez API.

Cała idea wyjątków polega na tym, że błąd zgłoszony gdzieś w dół łańcucha połączeń może pęknąć i zostać obsłużony przez kod gdzieś wyżej, bez ingerencji kodu. Z drugiej strony sprawdzone wyjątki wymagają, aby każdy poziom kodu między rzucającym a łapaczem deklarował, że wie o wszystkich formach wyjątków, które mogą przez nie przejść. To naprawdę niewiele różni się w praktyce od tego, czy sprawdzone wyjątki były po prostu specjalnymi wartościami zwracanymi, które sprawdzający musiał sprawdzić. np. [pseudokod]:

public [int or IOException] writeToStream(OutputStream stream) {
    [void or IOException] a= stream.write(mybytes);
    if (a instanceof IOException)
        return a;
    return mybytes.length;
}

Ponieważ Java nie może wykonywać alternatywnych wartości zwracanych ani prostych krotek wbudowanych jako wartości zwracanych, sprawdzone wyjątki są rozsądną odpowiedzią.

Problem polega na tym, że wiele kodu, w tym wielkie zbiory standardowej biblioteki, niewłaściwie używało sprawdzonych wyjątków dla naprawdę wyjątkowych warunków, które możesz chcieć złapać kilka poziomów wyżej. Dlaczego IOException nie jest RuntimeException? W każdym innym języku mogę pozwolić, aby wystąpił wyjątek we / wy, a jeśli nie zrobię nic, aby go obsłużyć, moja aplikacja zatrzyma się i dostanę przydatny ślad stosu do obejrzenia. To najlepsza rzecz, jaka może się zdarzyć.

Może dwie metody powyżej przykładu, w której chcesz wychwycić wszystkie wyjątki IO z całego procesu zapisu do strumienia, przerwać proces i przejść do kodu raportowania błędów; w Javie nie można tego zrobić bez dodawania „rzuca wyjątek IOException” na każdym poziomie połączenia, nawet na poziomach, które same nie wykonują IO. Takie metody nie powinny wymagać wiedzy na temat obsługi wyjątków; dodając wyjątki do swoich podpisów:

  1. niepotrzebnie zwiększa sprzężenie;
  2. sprawia, że ​​podpisy interfejsu są bardzo kruche, aby je zmienić;
  3. sprawia, że ​​kod jest mniej czytelny;
  4. jest tak irytujące, że powszechną reakcją programisty jest pokonanie systemu poprzez zrobienie czegoś okropnego, takiego jak „rzuca wyjątek”, „catch (Exception e) {}” lub zawijanie wszystkiego w RuntimeException (co utrudnia debugowanie).

I jest jeszcze wiele niedorzecznych wyjątków bibliotecznych, takich jak:

try {
    httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
    throw new CanNeverHappenException("oh dear!");
}

Kiedy musisz zaśmiecać swój kod tak absurdalnym dowcipem, nic dziwnego, że sprawdzone wyjątki są pełne nienawiści, chociaż tak naprawdę jest to po prostu kiepski projekt API.

Innym szczególnym złym skutkiem jest odwrócenie kontroli, gdzie komponent A dostarcza wywołanie zwrotne do ogólnego komponentu B. Komponent A chce być w stanie pozwolić wyjątkowi wyrzucić swój zwrot z powrotem do miejsca, w którym nazwał komponent B, ale nie może ponieważ zmieniłoby to interfejs wywołania zwrotnego, który jest naprawiony przez B. A może to zrobić tylko poprzez zawinięcie prawdziwego wyjątku w RuntimeException, który jest jeszcze bardziej wymagający do zapisania.

Sprawdzone wyjątki zaimplementowane w Javie i jej standardowej bibliotece oznaczają płytę kotłową, płytę kotłową, płytę kotłową. W już pełnym języku to nie jest wygrana.

Bobin
źródło
14
W twoim przykładzie kodu najlepiej byłoby połączyć łańcuchy wyjątków, aby podczas odczytu dzienników można było znaleźć pierwotną przyczynę: throw CanNeverHappenException (e);
Esko Luontola
5
Nie zgadzam się. Wyjątki, sprawdzone lub nie, są wyjątkowymi warunkami. Przykład: metoda, która pobiera obiekt przez HTTP. Zwracana wartość jest w innym przypadku przedmiotem lub niczym, wszystkie rzeczy, które mogą pójść źle, są wyjątkowe. Traktowanie ich jako wartości zwracanych, tak jak ma to miejsce w C, prowadzi tylko do zamieszania i złego projektu.
Mister Smith,
15
@Mister: Mówię, że sprawdzone wyjątki zaimplementowane w Javie zachowują się w praktyce bardziej jak wartości zwracane jak w C niż w tradycyjnych „wyjątkach”, które możemy rozpoznać po C ++ i innych językach wcześniejszych niż Java. I to IMO rzeczywiście prowadzi do zamieszania i złego projektu.
bobince
9
Zgadzam się, że standardowe biblioteki niewłaściwie wykorzystujące sprawdzone wyjątki zdecydowanie zwiększyły zamieszanie i złe zachowanie podczas łapania. I często jest to po prostu słaba dokumentacja, np. Metoda rozłączania, taka jak odłączanie (), która zgłasza wyjątek IOException, gdy „wystąpi jakiś inny błąd we / wy”. Cóż, rozłączałem się! Czy wyciekam uchwyt lub inny zasób? Czy muszę spróbować ponownie? Nie wiedząc, dlaczego tak się stało, nie mogę wyliczyć działania, które powinienem podjąć, więc muszę zgadywać, czy powinienem to połknąć, spróbować ponownie, czy zwolnić za kaucją.
charstar
14
+1 za „Alternatywne zwracane wartości API”. Ciekawy sposób patrzenia na sprawdzone wyjątki.
Zsolt Török,
75

Zamiast ponownie przeanalizować wszystkie (wiele) powodów przeciwko sprawdzonym wyjątkom, wybiorę tylko jeden. Nie pamiętam, ile razy napisałem ten blok kodu:

try {
  // do stuff
} catch (AnnoyingcheckedException e) {
  throw new RuntimeException(e);
}

99% czasu nie mogę nic na to poradzić. Wreszcie bloki wykonują wszelkie niezbędne porządki (a przynajmniej powinny).

Straciłem również liczbę wyświetleń:

try {
  // do stuff
} catch (AnnoyingCheckedException e) {
  // do nothing
}

Dlaczego? Ponieważ ktoś musiał sobie z tym poradzić i był leniwy. Czy to było złe? Pewnie. Czy to sie zdarza Absolutnie. Co jeśli byłby to niesprawdzony wyjątek? Aplikacja właśnie umarła (co jest lepsze niż połknięcie wyjątku).

A potem mamy irytujący kod, który wykorzystuje wyjątki jako formę kontroli przepływu, tak jak robi to java.text.Format . Bzzzt. Źle. Użytkownik wstawiający „abc” w pole liczbowe w formularzu nie jest wyjątkiem.

Ok, chyba trzy przyczyny.

Cletus
źródło
4
Ale jeśli wyjątek zostanie właściwie wychwycony, możesz poinformować użytkownika, wykonać inne zadania (zalogować?) I zamknąć aplikację w kontrolowany sposób. Zgadzam się, że niektóre części API mogły zostać zaprojektowane lepiej. A z powodu leniwego programisty myślę, że jako programista jesteś w 100% odpowiedzialny za swój kod.
Mister Smith,
3
zwróć uwagę, że try-catch-rethrow pozwala określić komunikat - zwykle używam go do dodawania informacji o zawartości zmiennych stanu. Częstym przykładem jest IOExceptions, aby dodać wartość bezwzględnąPathName () danego pliku.
Thorbjørn Ravn Andersen
15
Myślę, że IDE, takie jak Eclipse, mają wiele do winy za to, ile razy widziałeś pusty blok catch. Naprawdę powinny one ponownie rzucać.
artbristol,
12
„W 99% przypadków nic nie mogę na to poradzić” - źle, możesz pokazać użytkownikowi komunikat „Nie można połączyć się z serwerem” lub „Urządzenie IO nie powiodło się”, zamiast pozwolić na awarię aplikacji z powodu małej czkawki sieciowej. Oba twoje przykłady są dziełami złych programistów. Powinieneś atakować złych programistów i nie sprawdzać samych wyjątków. To tak, jakbym atakował insulinę za to, że nie pomagałem sobie z cukrzycą, kiedy używam jej jako sosu sałatkowego.
AxiomaticNexus
2
@YasmaniLlanes Nie zawsze możesz robić te rzeczy. Czasami masz interfejs do przestrzegania. Jest to szczególnie prawdziwe, gdy projektuje się dobre interfejsy API, które można łatwo utrzymać, ponieważ nie można po prostu rzucać wszędzie efektów ubocznych. Zarówno to, jak i złożoność, którą wprowadzi, będzie cię mocno gryźć na dużą skalę. Tak, w 99% przypadków nic nie można z tym zrobić.
MasterMastic,
50

Wiem, że to stare pytanie, ale spędziłem trochę czasu walcząc ze sprawdzonymi wyjątkami i muszę coś dodać. Proszę wybacz mi jej długość!

Moją główną wołowiną ze sprawdzonymi wyjątkami jest to, że rujnują polimorfizm. Niemożliwe jest, aby grały ładnie z interfejsami polimorficznymi.

Weź dobry stary Listinterfejs Java . Mamy wspólne implementacje w pamięci, takie jak ArrayListi LinkedList. Mamy również klasę szkieletową, AbstractListktóra ułatwia projektowanie nowych typów list. W przypadku listy tylko do odczytu musimy zaimplementować tylko dwie metody: size()i get(int index).

Ta przykładowa WidgetListklasa odczytuje niektóre obiekty o stałym rozmiarze typu Widget(nie pokazano) z pliku:

class WidgetList extends AbstractList<Widget> {
    private static final int SIZE_OF_WIDGET = 100;
    private final RandomAccessFile file;

    public WidgetList(RandomAccessFile file) {
        this.file = file;
    }

    @Override
    public int size() {
        return (int)(file.length() / SIZE_OF_WIDGET);
    }

    @Override
    public Widget get(int index) {
        file.seek((long)index * SIZE_OF_WIDGET);
        byte[] data = new byte[SIZE_OF_WIDGET];
        file.read(data);
        return new Widget(data);
    }
}

Udostępniając widżety za pomocą znanego Listinterfejsu, możesz pobierać elementy ( list.get(123)) lub iterować listę ( for (Widget w : list) ...) bez konieczności posiadania wiedzy o WidgetListsobie. Tę listę można przekazać dowolnej standardowej metodzie, która korzysta z list ogólnych, lub zawinąć ją w plik Collections.synchronizedList. Kod, który go używa, nie musi wiedzieć ani dbać o to, czy „widżety” są tworzone na miejscu, pochodzą z tablicy, czy są odczytywane z pliku, bazy danych, z sieci lub z przyszłego przekaźnika podprzestrzeni. Będzie nadal działać poprawnie, ponieważ Listinterfejs jest poprawnie zaimplementowany.

Tyle że nie jest. Powyższa klasa nie kompiluje się, ponieważ metody dostępu do plików mogą zgłaszać IOExceptionsprawdzony wyjątek, który należy „złapać lub określić”. Nie możesz podać go jako wyrzuconego - kompilator ci na to nie pozwoli, ponieważ naruszałoby to umowę Listinterfejsu. I nie ma użytecznego sposobu, który WidgetListmógłby sam poradzić sobie z wyjątkiem (jak wyjaśnię później).

Najwyraźniej jedyną rzeczą do zrobienia jest złapanie i ponowne sprawdzenie sprawdzonych wyjątków jako jakiś niezaznaczony wyjątek:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw new WidgetListException(e);
    }
}

public static class WidgetListException extends RuntimeException {
    public WidgetListException(Throwable cause) {
        super(cause);
    }
}

((Edycja: Java 8 dodała UncheckedIOExceptionklasę do dokładnie tego przypadku: do łapania i ponownego rzucania IOExceptions przez granice metod polimorficznych. To potwierdza mój punkt!)

Sprawdzone wyjątki po prostu nie działają w takich przypadkach. Nie możesz ich wyrzucić. To samo dotyczy sprytnego Mapwsparcia bazy danych lub implementacji java.util.Randompodłączonej do źródła kwantowej entropii za pośrednictwem portu COM. Gdy tylko spróbujesz zrobić coś nowego z implementacją interfejsu polimorficznego, koncepcja sprawdzonych wyjątków zawiedzie. Ale sprawdzone wyjątki są tak podstępne, że nadal nie pozostawiają cię w spokoju, ponieważ nadal musisz złapać i odrzucić dowolną z metod niższego poziomu, zaśmiecając kod i zaśmiecając stos.

Uważam, że wszechobecny Runnableinterfejs jest często cofany w tym rogu, jeśli wywołuje coś, co rzuca sprawdzone wyjątki. Nie może rzucać wyjątku w takiej formie, w jakiej jest, więc wszystko, co może zrobić, to zaśmiecać kod, przechwytując i ponownie pisząc jako RuntimeException.

W rzeczywistości możesz rzucać niezadeklarowane sprawdzone wyjątki, jeśli uciekasz się do hacków. JVM w czasie wykonywania nie przejmuje się sprawdzonymi regułami wyjątków, dlatego musimy oszukać tylko kompilator. Najprostszym sposobem na to jest nadużywanie leków generycznych. Oto moja metoda (nazwa klasy pokazana, ponieważ (przed Java 8) jest wymagana w składni wywołującej dla metody ogólnej):

class Util {
    /**
     * Throws any {@link Throwable} without needing to declare it in the
     * method's {@code throws} clause.
     * 
     * <p>When calling, it is suggested to prepend this method by the
     * {@code throw} keyword. This tells the compiler about the control flow,
     * about reachable and unreachable code. (For example, you don't need to
     * specify a method return value when throwing an exception.) To support
     * this, this method has a return type of {@link RuntimeException},
     * although it never returns anything.
     * 
     * @param t the {@code Throwable} to throw
     * @return nothing; this method never returns normally
     * @throws Throwable that was provided to the method
     * @throws NullPointerException if {@code t} is {@code null}
     */
    public static RuntimeException sneakyThrow(Throwable t) {
        return Util.<RuntimeException>sneakyThrow1(t);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> RuntimeException sneakyThrow1(
            Throwable t) throws T {
        throw (T)t;
    }
}

Hurra! Za pomocą tego możemy rzucić sprawdzony wyjątek na dowolną głębokość stosu bez deklarowania go, bez owijania go RuntimeExceptioni bez zaśmiecania śladu stosu! Korzystając ponownie z przykładu „WidgetList”:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw sneakyThrow(e);
    }
}

Niestety, ostatnią zniewagą sprawdzonych wyjątków jest to, że kompilator odmawia zezwolenia na złapanie sprawdzonego wyjątku, jeśli jego błędna opinia nie mogła zostać wyrzucona. (Niezaznaczone wyjątki nie mają tej reguły.) Aby złapać podstępnie zgłoszony wyjątek, musimy to zrobić:

try {
    ...
} catch (Throwable t) { // catch everything
    if (t instanceof IOException) {
        // handle it
        ...
    } else {
        // didn't want to catch this one; let it go
        throw t;
    }
}

Jest to nieco niezręczne, ale na plus jest wciąż nieco prostsze niż kod do wyodrębnienia sprawdzonego wyjątku, który został zawinięty w a RuntimeException.

Na szczęście throw t;instrukcja jest tutaj legalna, nawet jeśli typ tjest sprawdzany, dzięki dodanej w Javie 7 regule dotyczącej ponownego zgłaszania przechwyconych wyjątków.


Gdy sprawdzone wyjątki spotykają polimorfizm, problemem jest również odwrotny przypadek: gdy metoda jest określona jako potencjalnie rzucająca sprawdzony wyjątek, ale zastąpiona implementacja nie. Na przykład, klasa abstrakcyjna OutputStream„s writemetody wszystkim określić throws IOException. ByteArrayOutputStreamjest podklasą, która zapisuje do tablicy w pamięci zamiast prawdziwego źródła we / wy. Jego przesłonięte writemetody nie mogą powodować IOExceptions, więc nie mają throwsklauzuli i można je wywoływać, nie martwiąc się o wymaganie catch-or-spec.

Z wyjątkiem nie zawsze. Załóżmy, że Widgetistnieje metoda zapisania go w strumieniu:

public void writeTo(OutputStream out) throws IOException;

Właściwe jest zadeklarowanie tej metody jako akceptacji zwykłego OutputStream, więc można jej używać polimorficznie z wszelkiego rodzaju wyjściami: plikami, bazami danych, siecią i tak dalej. I tablice w pamięci. W przypadku tablicy w pamięci istnieje jednak fałszywy wymóg obsługi wyjątku, który w rzeczywistości nie może się zdarzyć:

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
    someWidget.writeTo(out);
} catch (IOException e) {
    // can't happen (although we shouldn't ignore it if it does)
    throw new RuntimeException(e);
}

Jak zwykle przeszkadzają sprawdzone wyjątki. Jeśli twoje zmienne są zadeklarowane jako typ podstawowy, który ma więcej otwartych wymagań dotyczących wyjątków, musisz dodać procedury obsługi tych wyjątków, nawet jeśli wiesz, że nie wystąpią one w Twojej aplikacji.

Ale czekaj, sprawdzone wyjątki są w rzeczywistości tak irytujące, że nawet nie pozwolą ci zrobić odwrotnie! Wyobraź sobie, że obecnie łapiesz wszystkie IOExceptionwyrzucane przez writepołączenia naOutputStream , ale chcesz zmienić typ deklarowanej zmiennej na a ByteArrayOutputStream, kompilator będzie cię krytykować za próbę złapania sprawdzonego wyjątku, o którym mówi, że nie można go wyrzucić.

Ta reguła powoduje pewne absurdalne problemy. Na przykład jedna z trzech writemetod nieOutputStream jest zastępowana przez . W szczególności jest to wygodna metoda, która zapisuje pełną tablicę, wywołując z przesunięciem 0 i długością tablicy. przesłania metodę trzyargumentową, ale dziedziczy metodę argumentacji wygodnej dla jednego argumentu taką, jaka jest. Dziedziczona metoda robi dokładnie to, co należy, ale zawiera niepożądaną klauzulę. Być może był to niedopatrzenie w projekcie , ale nigdy nie mogą go naprawić, ponieważ złamałoby to kompatybilność źródła z dowolnym kodem, który przechwytuje wyjątek - wyjątek, który nigdy nie jest, nigdy nie będzie i nigdy nie zostanie zgłoszony!ByteArrayOutputStreamwrite(byte[] data)write(byte[] data, int offset, int length)ByteArrayOutputStreamthrowsByteArrayOutputStream

Ta zasada jest irytująca podczas edycji i debugowania. Na przykład, czasami tymczasowo skomentuję wywołanie metody, a jeśli mogłoby ono zgłosić sprawdzony wyjątek, kompilator będzie teraz narzekał na istnienie lokalnego tryi catchbloków. Więc też muszę je skomentować, a teraz podczas edycji kodu IDE będzie wciskać do niewłaściwego poziomu, ponieważ {i }są komentowane. Gah! To niewielka skarga, ale wygląda na to, że jedyną rzeczą sprawdzoną w wyjątkach są kłopoty.


Już prawie skończyłem. Moja ostatnia frustracja sprawdzonymi wyjątkami polega na tym, że w większości witryn z telefonami nie ma nic użytecznego, co można z nimi zrobić. Idealnie, gdy coś pójdzie nie tak, dysponujemy kompetentnym, specyficznym dla aplikacji programem obsługi, który może poinformować użytkownika o problemie i / lub zakończyć lub ponowić operację w razie potrzeby. Może to zrobić tylko przewodnik z wysokiego stosu, ponieważ tylko on zna ogólny cel.

Zamiast tego otrzymujemy następujący idiom, który jest szalony jako sposób na zamknięcie kompilatora:

try {
    ...
} catch (SomeStupidExceptionOmgWhoCares e) {
    e.printStackTrace();
}

W GUI lub automatycznym programie drukowana wiadomość nie będzie widoczna. Co gorsza, leci wraz z resztą kodu po wyjątku. Czy wyjątek nie jest tak naprawdę błędem? Więc nie drukuj. W przeciwnym razie za chwilę wybuchnie coś innego, do tego czasu oryginalny obiekt wyjątku zniknie. Ten idiom nie jest lepszy niż BASIC On Error Resume Nextlub PHP error_reporting(0);.

Wywołanie jakiejś klasy loggera nie jest dużo lepsze:

try {
    ...
} catch (SomethingWeird e) {
    logger.log(e);
}

Jest to tak samo leniwe, jak e.printStackTrace();i nadal działa z kodem w nieokreślonym stanie. Ponadto wybór konkretnego systemu rejestrowania lub innej procedury obsługi jest specyficzny dla aplikacji, więc utrudnia to ponowne użycie kodu.

Ale poczekaj! Istnieje łatwy i uniwersalny sposób na znalezienie modułu obsługi specyficznego dla aplikacji. Jest wyżej w stosie wywołań (lub jest ustawiony jako nieprzechwycony moduł obsługi wyjątków w wątku ). Więc w większości miejsc wszystko, co musisz zrobić, to rzucić wyjątek wyżej na stos . Np throw e;. Sprawdzane wyjątki po prostu przeszkadzają.

Jestem pewien, że sprawdzone wyjątki wydawały się dobrym pomysłem, gdy język został zaprojektowany, ale w praktyce uważam, że wszystkie one przeszkadzają i nie mają żadnej korzyści.

Boann
źródło
W przypadku metody rozmiaru za pomocą WidgetList chciałbym buforować rozmiar w zmiennej i ustawić go w konstruktorze. Konstruktor może zgłosić wyjątek. Nie zadziała to, jeśli plik zmieni się podczas korzystania z WidgetList, co prawdopodobnie byłoby złe, gdyby tak się stało.
TofuBeer
2
SomeStupidExceptionOmgWhoCares dobrze ktoś troszczył się wystarczająco, aby go rzucić. Więc albo nigdy nie powinien był zostać wyrzucony (zły projekt), albo naprawdę powinieneś sobie z tym poradzić. To samo dotyczy złej implementacji klasy wcześniejszej niż 1.0 (strumień wyjściowy tablicy bajtów), w której projekt był niestety zły.
TofuBeer
2
Właściwym idiomem byłaby dyrektywa, która wychwytywałaby wszelkie określone wyjątki zgłaszane przez zagnieżdżone wywołania podprogramów i wrzuciłaby je ponownie w RuntimeException. Zauważ, że procedura może być jednocześnie zadeklarowana jako, throws IOExceptiona jednocześnie określać, że wszelkie IOExceptionwyrzucone z zagnieżdżonego wywołania należy uznać za nieoczekiwane i opakowane.
supercat
15
Jestem profesjonalnym programistą C # z pewnym doświadczeniem w Javie, który natknął się na ten post. Jestem zaskoczony, dlaczego ktoś poparłby to dziwne zachowanie. W .NET, jeśli chcę złapać określony typ wyjątku, mogę go złapać. Jeśli chcę po prostu pozwolić mu wyrzucić stos, nie mam nic do roboty. Chciałbym, żeby Java nie była tak dziwaczna. :)
aikeru
Jeśli chodzi o „czasami tymczasowo skomentuję wywołanie metody” - nauczyłem się if (false)do tego używać . Pozwala to uniknąć problemu z klauzulą ​​rzucania, a ostrzeżenie pomaga mi szybciej nawigować wstecz. +++ Powiedziawszy, zgadzam się ze wszystkim, co napisałeś. Sprawdzone wyjątki mają pewną wartość, ale ta wartość jest nieistotna w porównaniu do ich kosztu. Prawie zawsze przeszkadzają.
maaartinus
45

Cóż, nie chodzi o wyświetlanie śladu stosu lub dyskretne zawieszanie się. Chodzi o możliwość komunikowania błędów między warstwami.

Problem ze sprawdzonymi wyjątkami polega na tym, że zachęcają ludzi do połykania ważnych szczegółów (mianowicie klasy wyjątków). Jeśli zdecydujesz się nie połykać tego szczegółu, musisz ciągle dodawać deklaracje dotyczące rzutów w całej aplikacji. Oznacza to 1), że nowy typ wyjątku wpłynie na wiele sygnatur funkcji, i 2) możesz pominąć konkretną instancję wyjątku, który faktycznie chcesz - złapać (powiedz, że otwierasz plik pomocniczy dla funkcji, która zapisuje dane do plik pomocniczy jest opcjonalny, więc możesz zignorować jego błędy, ale ponieważ podpis throws IOExceptionjest łatwy do przeoczenia).

Naprawdę mam teraz do czynienia z tą sytuacją w aplikacji. Prawie wyjątki przepakowaliśmy jako AppSpecificException. Dzięki temu podpisy były naprawdę czyste i nie musieliśmy się martwić eksplozją throwspodpisów.

Oczywiście teraz musimy specjalizować się w obsłudze błędów na wyższych poziomach, wdrażając logikę ponownych prób i tym podobne. Wszystko jest jednak AppSpecificException, więc nie możemy powiedzieć „Jeśli zostanie zgłoszony wyjątek IOExe, spróbuj ponownie” lub „Jeśli zgłoszony zostanie ClassNotFound, przerwij całkowicie”. Nie mamy niezawodnego sposobu dotarcia do prawdziwego wyjątku, ponieważ rzeczy są ponownie pakowane, gdy przechodzą między naszym kodem a kodem strony trzeciej.

Dlatego jestem wielkim fanem obsługi wyjątków w Pythonie. Możesz złapać tylko to, co chcesz i / lub poradzić sobie. Wszystko inne pęka, jakbyś sam go rzucił (co i tak zrobiłeś).

Od czasu do czasu zauważyłem, że w całym projekcie, o którym wspominałem, obsługa wyjątków dzieli się na 3 kategorie:

  1. Złap i obsługuj określony wyjątek. Ma to na celu na przykład zaimplementowanie logiki ponownych prób.
  2. Złap i wrzuć inne wyjątki. Wszystko, co się tu dzieje, to zwykle logowanie i jest to zwykle banalna wiadomość, taka jak „Nie można otworzyć $ filename”. Są to błędy, na które nie można nic poradzić; tylko wyższe poziomy wiedzą wystarczająco dużo, aby sobie z tym poradzić.
  3. Złap wszystko i wyświetl komunikat o błędzie. Zwykle jest to samo sedno programu rozsyłającego, a wszystko, co robi, zapewnia komunikację błędu dzwoniącemu za pośrednictwem mechanizmu nie-wyjątku (wyskakujące okno dialogowe, zestawianie obiektu błędu RPC itp.).
Richard Levasseur
źródło
5
Mogłeś stworzyć określone podklasy wyjątku AppSpecificException, aby umożliwić separację przy jednoczesnym zachowaniu zwykłych podpisów metod.
Thorbjørn Ravn Andersen
1
Bardzo ważnym dodatkiem do pozycji 2 jest również to, że umożliwia DODAWANIE INFORMACJI do przechwyconego wyjątku (np. Poprzez zagnieżdżenie w RuntimeException). O wiele, wiele lepiej jest, aby nazwa pliku nie została znaleziona w śladzie stosu, niż ukryta głęboko w pliku dziennika.
Thorbjørn Ravn Andersen
1
Zasadniczo twój argument brzmi: „Zarządzanie wyjątkami jest męczące, więc wolałbym z tym nie radzić”. Gdy wyjątek pęka, traci on znaczenie, a tworzenie kontekstu jest praktycznie bezużyteczne. Jako projektant interfejsu API, który powinieneś zrobić, w umowie jest jasne, czego można się spodziewać, gdy coś pójdzie nie tak, jeśli mój program ulegnie awarii, ponieważ nie zostałem poinformowany, że ten lub inny wyjątek może „pęknąć”, a Ty, jako projektant, zawiodłeś i jako w wyniku twojej awarii mój system nie jest tak stabilny, jak to tylko możliwe.
Newtopian
4
To wcale nie to, co mówię. Twoje ostatnie zdanie faktycznie się ze mną zgadza. Jeśli wszystko jest zapakowane w AppSpecificException, wówczas nie powoduje to bąbelkowania (a znaczenie / kontekst zostaje utracone), i tak, klient API nie jest informowany - tak właśnie dzieje się ze sprawdzonymi wyjątkami (tak jak w Javie) , ponieważ ludzie nie chcą zajmować się funkcjami z dużą ilością throwsdeklaracji.
Richard Levasseur,
6
@Newtopian - wyjątki można w dużej mierze obsłużyć tylko na poziomie „biznesowym” lub „żądanie”. Rozsądne jest odrzucanie lub ponawianie próby przy dużej ziarnistości, nie dla każdej drobnej potencjalnej awarii. Z tego powodu najlepsze praktyki postępowania z wyjątkami są podsumowane jako „rzuć wcześnie, złap późno”. Sprawdzone wyjątki utrudniają zarządzanie niezawodnością na odpowiednim poziomie i zachęcają do wielu nieprawidłowo zakodowanych bloków catch. literatejava.com/exceptions/…
Thomas W
24

SNR

Po pierwsze, sprawdzone wyjątki zmniejszają „stosunek sygnału do szumu” dla kodu. Anders Hejlsberg mówi także o programowaniu imperatywnym i deklaratywnym, które jest podobną koncepcją. W każdym razie rozważ następujące fragmenty kodu:

Zaktualizuj interfejs użytkownika z nie-wątku interfejsu użytkownika w Javie:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

Zaktualizuj interfejs użytkownika z wątku innego niż interfejs użytkownika w języku C #:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

Co wydaje mi się o wiele wyraźniejsze. Kiedy zaczniesz robić coraz więcej pracy z interfejsem użytkownika w Swing, sprawdzone wyjątki stają się naprawdę denerwujące i bezużyteczne.

Jail Break

Aby wdrożyć nawet najbardziej podstawowe implementacje, takie jak interfejs listy Java, zaznaczono wyjątki jako narzędzie do projektowania według umowy. Rozważ listę, która jest wspierana przez bazę danych, system plików lub inną implementację, która zgłasza sprawdzony wyjątek. Jedyną możliwą implementacją jest złapanie zaznaczonego wyjątku i ponowne zwrócenie go jako niesprawdzonego wyjątku:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

A teraz musisz zapytać, jaki jest sens tego całego kodu? Sprawdzone wyjątki po prostu powodują hałas, wyjątek został wychwycony, ale nie został obsłużony, a projekt na podstawie umowy (pod względem sprawdzonych wyjątków) się zepsuł.

Wniosek

  • Łapanie wyjątków różni się od ich obsługi.
  • Zaznaczone wyjątki powodują szum w kodzie.
  • Obsługa wyjątków działa bezproblemowo w języku C #.

Pisałem o tym wcześniej .

Luke Quinane
źródło
3
W tym przykładzie zarówno Java, jak i C # pozwalają na rozpowszechnianie wyjątków bez ich obsługi (Java poprzez IllegalStateException). Różnica polega na tym, że możesz chcieć obsłużyć wyjątek FileNotFoundException, ale jest mało prawdopodobne, aby obsługa InvocationTargetException lub InterruptedException była przydatna.
Luke Quinane
3
I w sposób C # skąd mam wiedzieć, że może wystąpić wyjątek we / wy? Nigdy też nie rzuciłbym wyjątku od uruchomienia ... Uważam, że nadużywanie obsługi wyjątków. Przepraszam, ale za tę część kodu widzę jeszcze twoją stronę.
TofuBeer
7
Dojeżdżamy do tego miejsca :-) Więc z każdą nową wersją API musisz przeczesywać wszystkie połączenia i szukać nowych wyjątków, które mogą się zdarzyć? Może się to łatwo zdarzyć z wewnętrznymi interfejsami API firm, ponieważ nie muszą się one martwić kompatybilnością wsteczną.
TofuBeer
3
Czy chodziło Ci o zmniejszenie stosunku sygnału do szumu?
neo2862,
3
@TofuBeer Czy nie trzeba zmuszać się do aktualizacji kodu po zmianie interfejsu podkładającego interfejsu API? Gdybyś miał tam tylko niesprawdzone wyjątki, skończyłbyś z uszkodzonym / niekompletnym programem, nie wiedząc o tym.
Francois Bourgeois
23

Artima opublikował wywiad z jednym z architektów .NET, Andersiem Hejlsbergiem, który ostro omawia argumenty przeciwko sprawdzonym wyjątkom. Krótki degustator:

Klauzula throws, przynajmniej sposób, w jaki została zaimplementowana w Javie, niekoniecznie zmusza cię do obsługi wyjątków, ale jeśli nie obsłużysz ich, zmusza cię do dokładnego potwierdzenia, które wyjątki mogą przejść. Wymaga to złapania zadeklarowanych wyjątków lub umieszczenia ich we własnej klauzuli wyrzutów. Aby obejść ten wymóg, ludzie robią śmieszne rzeczy. Na przykład dekorują każdą metodę „rzuca wyjątek”. To po prostu całkowicie pokonuje tę funkcję, a ty po prostu zmusiłeś programistę do napisania bardziej pożółkłego gunka. To nikomu nie pomaga.

Le Dude
źródło
2
W rzeczywistości główny architekt.
Pułapka
18
Przeczytałem, że dla mnie jego argument sprowadza się do „tam są źli programiści”.
TofuBeer
6
TofuBeer, wcale nie. Chodzi o to, że wiele razy nie wiesz, co zrobić z wyjątkiem, który wywołuje wywoływana metoda, a sprawa, którą naprawdę jesteś zainteresowany, nie jest nawet wspomniana. Otwierasz plik, dostajesz wyjątek We / Wy, na przykład ... to nie jest mój problem, więc go wyrzucam. Ale metoda wywoływania najwyższego poziomu będzie po prostu chciała przerwać przetwarzanie i poinformować użytkownika o nieznanym problemie. Sprawdzony wyjątek wcale nie pomógł. To była jedna z milionów dziwnych rzeczy, które mogą się zdarzyć.
Dan Rosenstark
4
@yar, jeśli nie podoba ci się zaznaczony wyjątek, zrób „wyślij nowy wyjątek RuntimeException” („nie spodziewaliśmy się tego podczas wykonywania Foo.bar ()”, e) ”i skończmy z tym.
Thorbjørn Ravn Andersen
4
@ ThorbjørnRavnAndersen: Podstawową słabością projektową w Javie, którą niestety skopiowano .net, jest to, że wykorzystuje on typ wyjątku jako podstawowy sposób decydowania o tym, czy należy podjąć działania, oraz podstawowy sposób wskazywania ogólnego rodzaju rzeczy poszło nie tak, gdy w rzeczywistości te dwie kwestie są w dużej mierze ortogonalne. Liczy się nie to, co poszło nie tak, ale w jakich stanach znajdują się obiekty. Co więcej, zarówno .net, jak i Java domyślnie zakładają, że działanie i rozwiązywanie wyjątku są zasadniczo takie same, podczas gdy w rzeczywistości często są różne.
supercat
20

Początkowo zgodziłem się z tobą, ponieważ zawsze byłem za sprawdzonymi wyjątkami i zacząłem zastanawiać się, dlaczego nie lubię nie sprawdzać wyjątków w .Net. Ale potem zdałem sobie sprawę, że nie mam wpływu na sprawdzone wyjątki.

Aby odpowiedzieć na twoje pytanie, tak, podoba mi się, że moje programy pokazują ślady stosu, najlepiej naprawdę brzydkie. Chcę, aby aplikacja eksplodowała w straszną stertę najbrzydszych komunikatów o błędach, jakie kiedykolwiek chciałeś zobaczyć.

A powodem jest to, że jeśli to zrobi, muszę to naprawić i muszę to naprawić od razu. Chcę natychmiast wiedzieć, że jest problem.

Ile razy faktycznie zajmujesz się wyjątkami? Nie mówię o wyłapywaniu wyjątków - mówię o postępowaniu z nimi? Zbyt łatwo jest napisać:

try {
  thirdPartyMethod();
} catch(TPException e) {
  // this should never happen
}

Wiem, że możesz powiedzieć, że to zła praktyka i że „odpowiedzią” jest zrobienie czegoś z wyjątkiem (niech zgadnę, zaloguj się?), Ale w świecie rzeczywistym większość programistów po prostu tego nie robi. to.

Więc tak, nie chcę wychwytywać wyjątków, jeśli nie muszę tego robić, i chcę, aby mój program wybuchł spektakularnie, gdy coś spieprzę. Ciche niepowodzenie to najgorszy możliwy wynik.

Tsimon
źródło
Java zachęca do robienia takich rzeczy, abyś nie musiał dodawać każdego rodzaju wyjątku do każdej sygnatury metody.
yfeldblum
17
Zabawne ... odkąd poprawnie uwzględniłem sprawdzone wyjątki i odpowiednio je wykorzystałem, moje programy przestały wysadzać w powietrze ogromną kupkę niezadowolenia z twojej twarzy. Jeśli podczas opracowywania masz duży ślad brzydkiego złego stosu, klient jest również zobowiązany je zdobyć. Uwielbiam patrzeć na jego twarz, gdy widzi ArrayIndexOutOfBoundsException ze śladem stosu wysokich mil na swoim rozbitym systemie zamiast małego powiadomienia na pasku zadań, mówiącego, że nie można przeanalizować konfiguracji kolorów dla przycisku XYZ, więc zamiast tego użyto domyślnej wartości wzdłuż
Newtopian
1
Być może Java potrzebuje deklaracji „cantHandle”, która określałaby, że metoda lub blok kodu try / catch nie jest przygotowany do obsługi określonego wyjątku występującego w nim i że każdy taki wyjątek występujący za pomocą środków innych niż jawne rzut w ramach tej metody (w przeciwieństwie do metody wywoływanej) powinien być automatycznie zawijany i ponownie wprowadzany w RuntimeException. IMHO, zaznaczone wyjątki rzadko powinny propagować stos wywołań bez zawijania.
supercat
3
@Newtopian - Piszę serwer i oprogramowanie o wysokiej niezawodności i robię to od 25 lat. Moje programy nigdy nie wybuchły i pracuję z systemami finansowymi i wojskowymi o wysokiej dostępności, ponownych próbach i ponownych połączeniach. Mam absolutnie obiektywną podstawę do preferowania wyjątków czasu wykonywania. Sprawdzone wyjątki utrudniają stosowanie najlepszej praktyki „rzucaj wcześnie, łap późno”. Prawidłowa niezawodność i obsługa błędów są na poziomie „biznesowym”, „połączeniowym” lub „żądającym”. (Lub czasami podczas analizy danych). Sprawdzone wyjątki utrudniają prawidłowe działanie.
Thomas W
1
Wyjątki, o których tu mówisz, są RuntimeExceptionsnaprawdę nie do wyłapania i zgadzam się, że powinieneś pozwolić, aby program wybuchł. Wyjątki, które zawsze należy wychwytywać i obsługiwać, to sprawdzone wyjątki, takie jak IOException. Jeśli dostaniesz IOException, nie ma nic do naprawienia w kodzie; twój program nie powinien wybuchnąć tylko dlatego, że wystąpiła czkawka sieciowa.
AxiomaticNexus
20

Artykuł Skuteczne wyjątki Java ładnie wyjaśnia, kiedy należy używać niezaznaczonych, a kiedy sprawdzonych wyjątków. Oto kilka cytatów z tego artykułu, aby podkreślić główne punkty:

Nieprzewidziane okoliczności : oczekiwany warunek wymagający alternatywnej odpowiedzi od metody, który można wyrazić w kategoriach zamierzonego celu metody. Program wywołujący metodę oczekuje tego rodzaju warunków i ma strategię radzenia sobie z nimi.

Błąd: Nieplanowany stan, który uniemożliwia metodzie osiągnięcie zamierzonego celu, którego nie można opisać bez odniesienia do wewnętrznej implementacji metody.

(SO nie zezwala na tabele, więc możesz przeczytać poniższe informacje na oryginalnej stronie ...)

Przypadkowość

  • Uznaje się za: Część projektu
  • Oczekuje się, że tak się stanie: regularnie, ale rzadko
  • Kogo to obchodzi: kod źródłowy, który wywołuje metodę
  • Przykłady: Alternatywne tryby powrotu
  • Najlepsze mapowanie: sprawdzony wyjątek

Wina

  • Uważany jest za: paskudną niespodziankę
  • Oczekuje się, że tak się stanie: Nigdy
  • Kogo to obchodzi: ludzie, którzy muszą rozwiązać problem
  • Przykłady: błędy programowania, awarie sprzętu, błędy konfiguracji, brakujące pliki, niedostępne serwery
  • Najlepsze mapowanie: Niesprawdzony wyjątek
Esko Luontola
źródło
Wiem, kiedy ich użyć, chcę wiedzieć, dlaczego ludzie, którzy nie stosują się do tej rady ... nie stosują się do tej rady :-)
TofuBeer
Co to są błędy programistyczne i jak je odróżnić od błędów użytkowania ? Czy to błąd programowy, jeśli użytkownik przekaże błędne argumenty do programu? Z punktu widzenia Java może to nie być błąd programistyczny, ale z punktu widzenia skryptu powłoki jest to błąd programowy. Więc w czym są nieprawidłowe argumenty args[]? Czy to przypadek, czy awaria?
ceving 12.03.13
3
@TofuBeer - Ponieważ projektanci bibliotek Java postanowili umieścić wszelkiego rodzaju nieodwracalne awarie niskiego poziomu jako sprawdzone wyjątki, gdy wyraźnie powinny być odznaczone . FileNotFound to jedyny wyjątek IOException, który należy na przykład sprawdzić. W odniesieniu do JDBC - tylko łączenie się z bazą danych, można zasadnie uznać za przypadek . Wszystkie inne wyjątki SQLEx powinny być błędami i odznaczone. Obsługa błędów powinna być poprawnie na poziomie „biznesowym” lub „żądanie” - patrz najlepsza praktyka „rzucaj wcześnie, łap późno”. Sprawdzone wyjątki stanowią barierę.
Thomas W
1
W twojej argumentacji jest jedna wielka wada. „Awaryjne” NIE mogą być obsługiwane przez wyjątki, ale poprzez kod biznesowy i zwracane wartości metod. Wyjątek stanowią, jak to mówi słowo, WYJĄTKOWE sytuacje, a zatem Błędy.
Matteo Mosca
@MatteoMosca Kody zwrotne błędów zwykle są ignorowane i to wystarczy, aby je zdyskwalifikować. W rzeczywistości wszystko, co niezwykłe, może często być obsługiwane tylko gdzieś na stosie, i jest to przypadek użycia wyjątków. Mogę sobie wyobrazić coś takiego jak File#openInputStreampowrót Either<InputStream, Problem>- jeśli o to ci chodzi, to możemy się zgodzić.
maaartinus,
19

W skrócie:

Wyjątki stanowią pytanie projektowe API. -- Nie więcej nie mniej.

Argument dla sprawdzonych wyjątków:

Aby zrozumieć, dlaczego sprawdzone wyjątki mogą nie być dobrą rzeczą, odwróćmy to pytanie i zapytaj: Kiedy lub dlaczego sprawdzane wyjątki są atrakcyjne, tj. Dlaczego chcesz, aby kompilator wymuszał deklarację wyjątków?

Odpowiedź jest oczywista: czasami trzeba złapać wyjątek, a jest to możliwe tylko wtedy, gdy wywoływany kod oferuje określoną klasę wyjątków dla interesującego cię błędu.

Dlatego argumentem za sprawdzonymi wyjątkami jest to, że kompilator zmusza programistów do zadeklarowania, które wyjątki są zgłaszane, i mam nadzieję, że programista następnie udokumentuje również określone klasy wyjątków i błędy, które je powodują.

W rzeczywistości jednak pakiet zbyt często com.acmerzuca AcmeExceptionraczej podklasy niż określone. Dzwoniący muszą wtedy obsłużyć, zadeklarować lub ponownie zasygnalizować AcmeExceptions, ale nadal nie mogą być pewni, czy AcmeFileNotFoundErrorzdarzyło się, czy nie AcmePermissionDeniedError.

Więc jeśli jesteś zainteresowany tylko AcmeFileNotFoundError, rozwiązaniem jest złożenie wniosku o funkcję do programistów ACME i powiedzenie im, aby zaimplementowali, zadeklarowali i udokumentowali tę podklasę AcmeException.

Więc po co zawracać sobie głowę?

Dlatego nawet przy sprawdzonych wyjątkach kompilator nie może zmusić programistów do zgłaszania użytecznych wyjątków. To wciąż tylko kwestia jakości interfejsu API.

W rezultacie języki bez zaznaczonych wyjątków zwykle nie mają się gorzej. Programiści mogą mieć pokusę, by rzucić niespecyficzne wystąpienia Errorklasy ogólnej zamiast an AcmeException, ale jeśli w ogóle troszczą się o swoją jakość API, nauczą się wprowadzać AcmeFileNotFoundError.

Ogólnie rzecz biorąc, specyfikacja i dokumentacja wyjątków nie różni się zbytnio od specyfikacji i dokumentacji, powiedzmy, zwykłych metod. Te także są pytaniami projektowymi API i jeśli programista zapomniał zaimplementować lub wyeksportować użyteczną funkcję, interfejs API musi zostać ulepszony, aby można było z nim użytecznie pracować.

Postępując zgodnie z tym tokiem rozumowania, powinno być oczywiste, że „kłopot” z deklarowaniem, wyłapywaniem i ponownym zgłaszaniem wyjątków, który jest tak powszechny w językach takich jak Java, często wnosi niewielką wartość.

Warto również zauważyć, że maszyna wirtualna Java nie ma sprawdzonych wyjątków - sprawdza je tylko kompilator Java, a pliki klas ze zmienionymi deklaracjami wyjątków są kompatybilne w czasie wykonywania. Bezpieczeństwo VM VM nie jest poprawiane przez sprawdzone wyjątki, tylko styl kodowania.

David Lichteblau
źródło
4
Twój argument przemawia przeciwko sobie. Jeśli „czasami trzeba złapać wyjątek”, a jakość interfejsu API jest często słaba, bez sprawdzonych wyjątków nie będzie wiadomo, czy projektant zaniedbał udokumentowania, że ​​pewna metoda zgłasza wyjątek, który należy złapać. Połącz to z rzucaniem AcmeExceptionzamiast z AcmeFileNotFoundErrorpowodzeniem, aby dowiedzieć się, co zrobiłeś źle i gdzie musisz to złapać. Sprawdzone wyjątki zapewniają programistom odrobinę ochrony przed złym projektem API.
Eva
1
Projekt biblioteki Java popełnił poważne błędy. „Sprawdzone wyjątki” dotyczyły przewidywalnych i możliwych do odzyskania zdarzeń awaryjnych - takich jak brak pliku, brak połączenia. Nigdy nie były przeznaczone ani przystosowane do awarii systemowej niskiego poziomu. Byłoby dobrze wymusić otwarcie pliku do sprawdzenia, ale nie ma sensownego ponowienia lub odzyskania w przypadku niepowodzenia zapisu pojedynczego bajtu / wykonania zapytania SQL itp. Ponowna próba lub odzyskanie są poprawnie obsługiwane na żądanie „biznesowe” lub „ „poziom, który sprawdził wyjątki, utrudnia bezcelowo. literatejava.com/exceptions/…
Thomas W
17

W ciągu ostatnich trzech lat współpracowałem z kilkoma programistami w stosunkowo złożonych aplikacjach. Mamy bazę kodu, która dość często używa sprawdzonych wyjątków z odpowiednią obsługą błędów, a niektóre inne nie.

Do tej pory łatwiej mi było pracować z bazą kodu za pomocą sprawdzonych wyjątków. Kiedy korzystam z czyjegoś interfejsu API, miło jest widzieć dokładnie, jakiego rodzaju warunków błędu mogę się spodziewać, gdy wywołam kod i odpowiednio je obsłużę, logując się, wyświetlając lub ignorując (Tak, istnieją uzasadnione przypadki ignorowania wyjątki, takie jak implementacja ClassLoader). To daje kod, który piszę, możliwość odzyskania. Wszystkie wyjątki środowiska wykonawczego propaguję do momentu, aż zostaną zapisane w pamięci podręcznej i obsłużone jakimś ogólnym kodem obsługi błędów. Kiedy znajduję sprawdzony wyjątek, którego tak naprawdę nie chcę obsłużyć na określonym poziomie lub który rozważam błąd logiki programowania, to zawijam go w wyjątek RuntimeException i pozwalam, aby wystąpił błąd. Nigdy, nigdy nie połykaj wyjątku bez uzasadnionego powodu (a dobrych powodów, aby to zrobić, jest raczej mało)

Kiedy pracuję z bazą kodu, która nie ma sprawdzonych wyjątków, nieco trudniej mi jest wiedzieć z wyprzedzeniem, czego mogę się spodziewać podczas wywoływania funkcji, co może strasznie zepsuć niektóre rzeczy.

Jest to oczywiście kwestia preferencji i umiejętności programistycznych. Oba sposoby programowania i obsługi błędów mogą być równie skuteczne (lub nieskuteczne), więc nie powiedziałbym, że istnieje One Way.

Podsumowując, łatwiej mi pracować ze sprawdzonymi wyjątkami, szczególnie w dużych projektach z dużą liczbą programistów.

Mario Ortegón
źródło
6
Robię by. Dla mnie są istotną częścią umowy. Bez konieczności sięgania do szczegółów w dokumentacji API, mogę szybko poznać najbardziej prawdopodobne scenariusze błędów.
Wayne Hartman
2
Zgodzić się. Kiedy próbowałem nawiązywać połączenia sieciowe, doświadczyłem konieczności sprawdzania wyjątków w .Net. Wiedząc, że czkawka sieci może się zdarzyć w dowolnym momencie, musiałem przeczytać całą dokumentację API, aby dowiedzieć się, jaki wyjątek muszę złapać specjalnie dla tego scenariusza. Gdyby C # sprawdził wyjątki, wiedziałbym natychmiast. Inni programiści C # prawdopodobnie po prostu pozwoliliby awarii aplikacji na zwykły błąd sieci.
AxiomaticNexus
13

Kategorie wyjątków

Mówiąc o wyjątkach, zawsze powracam do artykułu Erica Lipperta na blogu wyjątków Vexing . Stawia wyjątki w tych kategoriach:

  • Śmiertelne - wyjątki te nietwoją winą : nie możesz temu zapobiec i nie możesz rozsądnie sobie z nimi poradzić. Na przykład OutOfMemoryErrorlub ThreadAbortException.
  • Boneheaded - Te wyjątki są twoją winą : powinieneś im zapobiec, a one reprezentują błędy w twoim kodzie. Na przykład ArrayIndexOutOfBoundsException, NullPointerExceptionczy każdy IllegalArgumentException.
  • Vexing - te wyjątki niewyjątkowe , nie twoja wina, nie możesz im zapobiec, ale będziesz musiał sobie z nimi poradzić. Są one często wynikiem nieszczęśliwego decyzji projektowej, takich jak rzucanie NumberFormatExceptionze Integer.parseIntzamiast zapewnienie Integer.tryParseIntmetody, która zwraca wartość logiczną FALSE w przypadku błędu parsowania.
  • Egzogeniczne - wyjątki te są zazwyczaj wyjątkowe , a nie twoja wina, nie możesz (racjonalnie) im zapobiec, ale musisz sobie z nimi poradzić . Na przykład FileNotFoundException.

Użytkownik interfejsu API:

  • nie może obsługiwać wyjątków śmiertelnych lub bez kości .
  • powinny obsługiwać dokuczliwe wyjątki, ale nie powinny występować w idealnym interfejsie API.
  • musi obsługiwać egzogeniczne wyjątki.

Sprawdzone wyjątki

Fakt, że użytkownik interfejsu API musi obsłużyć określony wyjątek, jest częścią umowy dotyczącej metody między dzwoniącym a odbiorcą. Umowa określa między innymi: liczbę i rodzaje argumentów, których oczekuje osoba odbierająca, rodzaj wartości zwracanej, której może oczekiwać osoba dzwoniąca, oraz wyjątki, z którymi osoba dzwoniąca ma się zapoznać .

Ponieważ w interfejsie API nie powinny istnieć dokuczliwe wyjątki, tylko te wyjątki egzogeniczne muszą być sprawdzone, aby były częścią kontraktu metody. Stosunkowo niewiele wyjątków jest egzogennych , więc każdy interfejs API powinien mieć stosunkowo niewiele sprawdzonych wyjątków.

Zaznaczony wyjątek to wyjątek, który należy obsłużyć . Obsługa wyjątku może być tak prosta, jak jego przełknięcie. Tam! Obsługiwany jest wyjątek. Kropka. Jeśli deweloper chce sobie z tym poradzić, dobrze. Ale nie może zignorować wyjątku i został ostrzeżony.

Problemy z interfejsem API

Ale każdy interfejs API, który sprawdził irytujące i krytyczne wyjątki (np. JCL), niepotrzebnie obciąży użytkowników interfejsu API. Takie wyjątki muszą zostać obsłużone, ale albo wyjątek jest tak powszechny, że nie powinien był być wyjątkiem, ani nic nie można zrobić przy jego obsłudze. A to powoduje, że programiści Java nienawidzą sprawdzonych wyjątków.

Ponadto wiele interfejsów API nie ma odpowiedniej hierarchii klas wyjątków, co powoduje, że wszystkie rodzaje egzogennych wyjątków są reprezentowane przez pojedynczą sprawdzoną klasę wyjątków (np. IOException .). A to powoduje, że programiści Java nienawidzą sprawdzonych wyjątków.

Wniosek

Egzogenne wyjątki to te, które nie są twoją winą, których nie można było zapobiec i którym należy się zająć. Stanowią one niewielki podzbiór wszystkich wyjątków, które mogą zostać zgłoszone. Interfejsy API powinny sprawdzać wyłącznie wyjątki egzogeniczne , a wszystkie pozostałe wyjątki niezaznaczone. Spowoduje to lepsze interfejsy API, zmniejszy obciążenie użytkownika interfejsu API, a tym samym zmniejszy potrzebę wychwytywania wszystkich, połykania lub ponownego odrzucania niezaznaczonych wyjątków.

Więc nie nienawidzę Java i jej sprawdzonych wyjątków. Zamiast tego nienawidzą interfejsów API, które nadużywają sprawdzonych wyjątków.

Daniel AA Pelsmaeker
źródło
I niewłaściwie je wykorzystuj, nie mając hierarchii.
TofuBeer
1
FileNotFound i nawiązanie połączenia JDBC / sieciowego to nieprzewidziane zdarzenia, które należy sprawdzić, wyjątki, ponieważ są one przewidywalne i (możliwe) do odzyskania. Większość innych wyjątków IOExceptions, SQLExceptions, RemoteException itp. Są nieprzewidywalnymi i nieodwracalnymi awariami i powinny być wyjątkami czasu wykonywania. Ze względu na błędny projekt biblioteki Java, wszyscy mieliśmy ten błąd i teraz używamy głównie Spring & Hibernate (który poprawnie zaprojektował).
Thomas W
Zwykle powinieneś sobie poradzić z wyjątkami z kością, chociaż możesz nie chcieć nazywać tego „obsługą”. Na przykład na serwerze WWW loguję je i wyświetlam 500 użytkownikowi. Ponieważ wyjątek jest nieoczekiwany, przed naprawą błędu jest wszystko, co mogę zrobić.
maaartinus,
6

Ok ... Sprawdzone wyjątki nie są idealne i mają pewne zastrzeżenie, ale służą celowi. Podczas tworzenia interfejsu API występują określone przypadki awarii, które są umowne dla tego interfejsu API. Jeśli w kontekście silnie statycznego języka, takiego jak Java, jeśli nie używa się sprawdzonych wyjątków, należy polegać na dokumentacji i konwencji ad hoc, aby przekazać możliwość wystąpienia błędu. Takie postępowanie usuwa wszelkie korzyści, które kompilator może przynieść w obsłudze błędów, i pozostajesz całkowicie w dobrej woli programistów.

Tak więc, usuwa się sprawdzony wyjątek, tak jak to zrobiono w języku C #, jak zatem można programowo i strukturalnie przekazać możliwość błędu? Jak poinformować kod klienta, że ​​takie i takie błędy mogą wystąpić i muszą zostać usunięte?

Słyszę różnego rodzaju okropności, gdy mamy do czynienia ze sprawdzonymi wyjątkami, są niewłaściwie wykorzystywane, to pewne, ale tak samo nie są zaznaczone wyjątki. Mówię, poczekaj kilka lat, gdy interfejsy API zostaną ułożone w stos wiele warstw głęboko, a będziesz błagać o zwrot jakiegoś uporządkowanego środka do przenoszenia awarii.

Weźmy przypadek, gdy wyjątek został zgłoszony gdzieś u dołu warstw API i po prostu pojawił się bąbelek, ponieważ nikt nie wiedział, że może nawet wystąpić ten błąd, mimo że był to rodzaj błędu, który był bardzo prawdopodobny, gdy kod wywołujący wyrzuciłem go (na przykład FileNotFoundException w przeciwieństwie do VogonsTrashingEarthExcept ... w takim przypadku nie miałoby znaczenia, czy będziemy to robić, czy nie, ponieważ nie ma już nic do obsługi).

Wielu twierdziło, że niemożność załadowania pliku była prawie zawsze końcem świata dla tego procesu i musi umrzeć straszną i bolesną śmiercią. Więc tak ... pewnie ... ok ... budujesz API dla czegoś i w pewnym momencie ładuje on plik ... Ja jako użytkownik wspomnianego API mogę tylko odpowiedzieć ... "Kim do diabła jesteś, kiedy decydujesz program powinien ulec awarii! ” Pewnie Biorąc pod uwagę wybór, w którym wyjątki są pochłaniane i nie pozostawiają śladu, lub wyjątek EletroFlabbingChunkFluxManifoldChuggingException ze śladem stosu głębszym niż rów Marianna, wybrałbym to ostatnie bez wahania, ale czy to oznacza, że ​​jest to pożądany sposób radzenia sobie z wyjątkiem ? Czy nie możemy być gdzieś pośrodku, gdzie wyjątek zostałby przekształcony i zawinięty za każdym razem, gdy przejdzie on na nowy poziom abstrakcji, aby rzeczywiście coś znaczył?

Wreszcie większość argumentów, które widzę, brzmi: „Nie chcę zajmować się wyjątkami, wiele osób nie chce zajmować się wyjątkami. Sprawdzone wyjątki zmuszają mnie do radzenia sobie z nimi, dlatego nienawidzę sprawdzanego wyjątku” Aby całkowicie wyeliminować taki mechanizm i sprowadzenie go do otchłani goto piekła jest po prostu głupie i pozbawione żonglowania i wizji.

Jeśli wyeliminujemy sprawdzony wyjątek, możemy również wyeliminować typ zwracany przez funkcje i zawsze zwracać zmienną „anytype” ... To uczyniłoby życie o wiele prostszym, prawda?

Newtopian
źródło
2
Sprawdzone wyjątki byłyby przydatne, gdyby istniał deklaratywny sposób powiedzenia, że ​​żadne wywołanie metody w bloku nie spowoduje wygenerowania niektórych (lub dowolnych) sprawdzonych wyjątków, a wszelkie takie wyjątki powinny być automatycznie opakowane i ponownie wprowadzone. Mogą być jeszcze bardziej przydatne, jeśli wywołania metod, które zostały zadeklarowane jako zgłaszanie sprawdzonych wyjątków, zastąpiły szybkość / zwrot wywołania dla prędkości obsługi wyjątków (tak aby oczekiwane wyjątki mogły być obsługiwane prawie tak szybko, jak normalny przepływ programu). Jednak żadna z tych sytuacji obecnie nie ma zastosowania.
supercat
6

Rzeczywiście, sprawdzone wyjątki z jednej strony zwiększają niezawodność i poprawność programu (jesteś zmuszony do składania poprawnych deklaracji swoich interfejsów - wyjątki, które zgłasza metoda, są w zasadzie specjalnym rodzajem zwrotu). Z drugiej strony napotykasz problem polegający na tym, że skoro wyjątki „wybuchają”, bardzo często musisz zmienić wiele metod (wszystkich dzwoniących, dzwoniących i tak dalej) po zmianie wyjątków. metoda rzuca.

Sprawdzone wyjątki w Javie nie rozwiązują tego drugiego problemu; C # i VB.NET wyrzucają dziecko z kąpielą.

Ładne podejście, które kroczy środkową drogą, zostało opisane w tym dokumencie OOPSLA 2005 (lub w powiązanym raporcie technicznym ).

Krótko mówiąc, pozwala powiedzieć:, method g(x) throws like f(x)co oznacza, że ​​g wyrzuca wszystkie wyjątki. Voila, sprawdzone wyjątki bez problemu ze zmianami kaskadowymi.

Chociaż jest to praca naukowa, zachęcam do przeczytania (jej części), ponieważ dobrze wyjaśnia, jakie są zalety i wady sprawdzonych wyjątków.

Kurt Schelfthout
źródło
5

To nie jest argument przeciwko czystej koncepcji sprawdzonych wyjątków, ale hierarchia klas, którą wykorzystuje dla nich Java, to dziwaczny pokaz. Zawsze nazywamy to po prostu „wyjątkiem” - co jest poprawne, ponieważ specyfikacja języka nazywa je również - ale jak nazywa się i reprezentuje wyjątek w systemie typów?

Przez klasę Exceptionmożna sobie wyobrazić? Cóż, nie, ponieważ Exceptions są wyjątkami i podobnie wyjątki są Exceptions, z wyjątkiem wyjątków, które nieException, ponieważ inne wyjątki są faktycznie Errors, które są innym rodzajem wyjątku, rodzajem wyjątkowego wyjątku, który nigdy nie powinien się zdarzyć, z wyjątkiem kiedy tak się dzieje i których nigdy nie powinieneś łapać, chyba że czasem musisz. Tyle że to nie wszystko, ponieważ możesz także zdefiniować inne wyjątki, które nie są ani Exceptions, ani Errorzwykłymi Throwablewyjątkami.

Które z nich to „sprawdzone” wyjątki? Throwables są sprawdzanymi wyjątkami, z wyjątkiem tych Error, które są również wyjątkami odznaczonymi, a następnie są Exceptions, które są również Throwables i są głównym rodzajem sprawdzanego wyjątku, z wyjątkiem tego, że jest też jeden wyjątek, to znaczy, że jeśli są również RuntimeExceptions, ponieważ jest to inny rodzaj niesprawdzonego wyjątku.

Po co są RuntimeException? Cóż, jak sama nazwa wskazuje, są wyjątkami, jak wszystkie Exceptions, i zdarzają się w czasie wykonywania, tak jak wszystkie wyjątki, z wyjątkiem tego, że RuntimeExceptionsą wyjątkowe w porównaniu z innymi, Exceptionponieważ nie powinny się zdarzyć, z wyjątkiem kiedy popełnisz jakiś głupi błąd, chociaż RuntimeExceptionnigdy nie są Error, więc dotyczą one rzeczy wyjątkowo błędnych, ale tak naprawdę nie są Error. Z wyjątkiem RuntimeErrorException, co naprawdę jest RuntimeExceptionza Error. Ale czy nie wszystkie wyjątki mają reprezentować błędne okoliczności? Tak, wszystkie z nich. Z wyjątkiem ThreadDeathwyjątkowo wyjątkowego wyjątku, ponieważ dokumentacja wyjaśnia, że ​​jest to „normalne zjawisko” i że „Error

W każdym razie, ponieważ dzielimy wszystkie wyjątki na środku na Errors (które są wyjątkami wyjątkowego wykonania, więc niezaznaczone) i Exceptions (które dotyczą mniej wyjątkowych błędów wykonania, więc sprawdzane, z wyjątkiem sytuacji, gdy nie są), potrzebujemy teraz dwóch różne rodzaje każdego z kilku wyjątków. Potrzebujemy więc IllegalAccessErrori IllegalAccessException, i, InstantiationErrori InstantiationException, NoSuchFieldErrori NoSuchFieldException, i NoSuchMethodErrori NoSuchMethodExceptioni ZipErrori ZipException.

Tyle że nawet gdy sprawdzany jest wyjątek, zawsze istnieją (dość łatwe) sposoby oszukiwania kompilatora i wyrzucania go bez sprawdzania. Jeśli tak, że można uzyskać UndeclaredThrowableException, oprócz innych przypadkach, gdy mogłoby to zwymiotować jako UnexpectedException, albo UnknownException(co nie ma związku UnknownError, który jest tylko dla „poważnych wyjątków”), albo ExecutionException, albo InvocationTargetException, albo ExceptionInInitializerError.

Aha, i nie możemy zapomnieć o niesamowitej nowości Javy 8 UncheckedIOException, która jest RuntimeExceptionwyjątkiem zaprojektowanym, aby umożliwić ci wyrzucenie przez okno koncepcji sprawdzania wyjątków przez zawijanie sprawdzonych IOExceptionwyjątków spowodowanych błędami we / wy (które nie powodują IOErrorwyjątków, chociaż takie istnieją) też), które są wyjątkowo trudne w obsłudze, dlatego nie trzeba ich sprawdzać.

Dzięki Java!

Boann
źródło
2
O ile mogę powiedzieć, ta odpowiedź mówi tylko: „wyjątki Javy to bałagan” w pełnym ironii, zapewne zabawnym sposobie. To, co wydaje się nie robić, to wyjaśnia, dlaczego programiści unikają prób zrozumienia, jak te rzeczy powinny działać. Ponadto, w rzeczywistych przypadkach (przynajmniej tych, z którymi miałem okazję poradzić sobie), jeśli programiści nie celowo starają się utrudnić swoje życie, wyjątki nie są tak skomplikowane, jak opisałeś.
CptBartender
5

Problem

Najgorszy problem, jaki widzę z mechanizmem obsługi wyjątków, polega na tym, że wprowadza on duplikację kodu na dużą skalę ! Bądźmy szczerzy: w większości projektów w 95% przypadków programiści naprawdę muszą zrobić wyjątek, aby w jakiś sposób przekazać je użytkownikowi (aw niektórych przypadkach także zespołowi programistycznemu, np. Wysyłając e -mail ze śladem stosu). Tak więc zwykle w każdym miejscu obsługiwanym wyjątkiem jest używana ta sama linia / blok kodu.

Załóżmy, że wykonujemy proste logowanie w każdym bloku catch dla pewnego rodzaju sprawdzonego wyjątku:

try{
   methodDeclaringCheckedException();
}catch(CheckedException e){
   logger.error(e);
}

Jeśli jest to częsty wyjątek, może istnieć nawet kilkaset takich bloków try-catch w większej bazie kodu. Załóżmy teraz, że zamiast logowania do konsoli musimy wprowadzić obsługę wyjątków opartą na wyskakującym oknie dialogowym lub rozpocząć dodatkowe wysyłanie wiadomości e-mail do zespołu programistów.

Chwileczkę ... czy naprawdę będziemy edytować wszystkie te kilkaset lokalizacji w kodzie ?! Dostajesz mój punkt :-).

Rozwiązanie

Aby rozwiązać ten problem, wprowadziliśmy koncepcję modułów obsługi wyjątków (które będę dalej określać jako EH) w celu scentralizowania obsługi wyjątków. Do każdej klasy, która musi obsłużyć wyjątki, instancja procedury obsługi wyjątków jest wstrzykiwana przez naszą strukturę Dependency Injection . Typowy wzorzec obsługi wyjątków wygląda teraz tak:

try{
    methodDeclaringCheckedException();
}catch(CheckedException e){
    exceptionHandler.handleError(e);
}

Teraz, aby dostosować obsługę wyjątków, musimy zmienić kod tylko w jednym miejscu (kod EH).

Oczywiście w bardziej złożonych przypadkach możemy zaimplementować kilka podklas EH i funkcji dźwigni, które zapewnia nam nasza platforma DI. Zmieniając konfigurację frameworku DI, możemy łatwo przełączać implementację EH globalnie lub dostarczać konkretne implementacje EH do klas o specjalnych potrzebach obsługi wyjątków (na przykład za pomocą adnotacji Guice @Named).

W ten sposób możemy odróżnić zachowanie obsługi wyjątków w programowaniu i wydaniu wersji aplikacji (np. Programowanie - rejestrowanie błędu i zatrzymywanie aplikacji, prod - rejestrowanie błędu z większą ilością szczegółów i pozwalanie aplikacji na kontynuowanie działania) bez wysiłku.

Ostatnia rzecz

Wreszcie może się wydawać, że ten sam rodzaj centralizacji można uzyskać, przekazując nasze wyjątki „do góry”, aż dotrą one do klasy obsługi wyjątków najwyższego poziomu. Prowadzi to jednak do zaśmiecania kodu i podpisów naszych metod oraz wprowadza problemy konserwacyjne wspomniane przez innych w tym wątku.

Piotr Sobczyk
źródło
6
Wymyślono wyjątki, aby zrobić z nimi coś pożytecznego. Zapisanie ich w pliku dziennika lub renderowanie ładnego okna nie jest przydatne, ponieważ pierwotny problem nie został przez to rozwiązany. Wykonanie czegoś pożytecznego wymaga wypróbowania innej strategii rozwiązania. Przykłady: Jeśli nie mogę pobrać danych z serwera AI, wypróbuj je na serwerze B. Lub jeśli algorytm A powoduje przepełnienie sterty, wypróbuję algorytm B, który jest znacznie wolniejszy, ale może się powieść.
ceving 12.03.13
2
@ceving Tak, to wszystko jest dobre i prawdziwe w teorii. Ale teraz wróćmy do ćwiczenia słów. Proszę odpowiedzieć naprawdę szczerze, jak często robisz to w swoim prawdziwym projekcie? Jaka część catchbloków w tym prawdziwym projekcie robi coś naprawdę „użytecznego” z wyjątkiem wyjątków? 10% byłoby dobrze. Zwykłe problemy, które generują wyjątki, to próba odczytania konfiguracji z pliku, który nie istnieje, błędy OutOfMemoryErrors, wyjątki NullPointerException, błędy integralności ograniczenia bazy danych itp. Czy naprawdę próbujesz odzyskać wszystkie funkcje z wdziękiem? Nie wierzę ci :). Często nie ma sposobu na odzyskanie.
Piotr Sobczyk
3
@PiotrSobczyk: Jeśli program podejmie jakieś działanie w wyniku żądania suer, a operacja zakończy się niepowodzeniem w sposób, który nie uszkodził niczego w stanie systemu, powiadomienie użytkownika, że ​​operacja nie może zostać zakończona, jest całkowicie przydatna sposób radzenia sobie z sytuacją. Największą wadą wyjątków w C # i .net jest to, że nie ma spójnego sposobu ustalenia, czy coś w stanie systemu mogło zostać uszkodzone.
supercat
4
Prawidłowo @PiotrSobczyk. Najczęściej jedynym prawidłowym działaniem, które należy podjąć w odpowiedzi na wyjątek, jest wycofanie transakcji i zwrócenie odpowiedzi na błąd. Pomysły dotyczące „rozwiązania wyjątku” sugerują wiedzę i autorytet, którego nie mamy (i nie powinniśmy), oraz naruszają hermetyzację. Jeśli nasza aplikacja nie jest DB, nie powinniśmy próbować naprawić DB. Niepowodzenie w czystości i unikanie zapisu błędnych danych, ceving, jest wystarczająco przydatne .
Thomas W
@PiotrSobczyk Wczoraj miałem do czynienia z wyjątkiem „nie mogłem odczytać obiektu” (który powstanie tylko dlatego, że baza danych została zaktualizowana przed oprogramowaniem - co nigdy nie powinno się zdarzyć, ale jest możliwe z powodu błędu ludzkiego) przez awarię historyczna wersja bazy danych z pewnością wskazuje na starą wersję obiektu.
Tytułowy
4

Anders mówi o pułapkach sprawdzonych wyjątków i o tym, dlaczego zostawił je poza C # w odcinku 97 radia Inżynieria oprogramowania.

Chuck Conway
źródło
4

Mój zapis w witrynie c2.com jest nadal w większości niezmieniony w stosunku do oryginalnej wersji: CheckedExceptionsAreIncompatibleWithVisitorPattern

W podsumowaniu:

Wzorzec gościa i jego krewni są klasą interfejsów, w których zarówno pośredni wywołujący, jak i implementacja interfejsu wiedzą o wyjątku, ale interfejs i bezpośredni wywołujący tworzą bibliotekę, która nie może o tym wiedzieć.

Podstawowym założeniem CheckedExceptions jest to, że wszystkie deklarowane wyjątki mogą być zgłaszane z dowolnego punktu, który wywołuje metodę z tą deklaracją. VisitorPattern ujawnia, że ​​założenie to jest błędne.

Ostatecznym rezultatem sprawdzonych wyjątków w takich przypadkach jest dużo niepotrzebnego kodu, który zasadniczo usuwa ograniczenie sprawdzanego wyjątku kompilatora w czasie wykonywania.

Jeśli chodzi o podstawowy problem:

Moją ogólną ideą jest to, że moduł obsługi najwyższego poziomu musi interpretować wyjątek i wyświetlać odpowiedni komunikat o błędzie. Prawie zawsze widzę albo wyjątki we / wy, wyjątki komunikacyjne (z jakiegoś powodu rozróżniają interfejsy API) lub krytyczne błędy (błędy programu lub poważny problem na serwerze kopii zapasowej), więc nie powinno to być zbyt trudne, jeśli pozwolimy na śledzenie stosu dla poważnego problem z serwerem.

Jozuego
źródło
1
Powinieneś mieć coś takiego jak DAGNodeException w interfejsie, a następnie złap IOException i przekonwertuj go na DAGNodeException: publiczne wywołanie nieważności (DAGNode arg) zgłasza DAGNodeException;
TofuBeer
2
@TofuBeer, właśnie o to mi chodzi. Uważam, że ciągłe zawijanie i rozpakowywanie wyjątków jest gorsze niż usuwanie sprawdzonych wyjątków.
Joshua
2
Cóż, całkowicie się nie zgadzamy ... ale twój artykuł wciąż nie odpowiada na prawdziwe pytanie, w jaki sposób powstrzymujesz aplikację przed wyświetlaniem śladu stosu użytkownikowi, gdy zostanie zgłoszony wyjątek czasu wykonywania.
TofuBeer,
1
@TofuBeer - Gdy się nie powiedzie, mówienie użytkownikowi, że się nie udało, jest poprawne! Jaka jest twoja alternatywa, oprócz „nadpisania” błędu „zerowymi” lub niekompletnymi / niepoprawnymi danymi? Udawanie, że się udało, to kłamstwo, które tylko pogarsza sytuację. Dzięki 25-letniemu doświadczeniu w systemach o wysokiej niezawodności, logikę ponownych prób należy stosować ostrożnie i tam, gdzie to właściwe. Spodziewałbym się również, że Odwiedzający prawdopodobnie ponownie zawiedzie, bez względu na to, ile razy spróbujesz ponownie. O ile nie lecisz samolotem, zamiana na drugą wersję tego samego algorytmu jest niepraktyczna i nieprawdopodobna (i może się nie powieść).
Thomas W
4

Aby spróbować odpowiedzieć tylko na pytanie bez odpowiedzi:

Jeśli rzucisz podklasy RuntimeException zamiast podklas Exception, to skąd wiesz, co powinieneś złapać?

Pytanie zawiera podstępne uzasadnienie IMHO. To, że interfejs API mówi ci, co wyrzuca, nie oznacza, że ​​postępujesz z nim w ten sam sposób we wszystkich przypadkach. Innymi słowy, wyjątki, które musisz wychwycić, różnią się w zależności od kontekstu, w którym używasz komponentu zgłaszającego wyjątek.

Na przykład:

Jeśli piszę tester połączenia dla bazy danych lub coś, aby sprawdzić poprawność wpisanego przez użytkownika XPath, prawdopodobnie chciałbym wychwycić i zgłosić wszystkie sprawdzone i niezaznaczone wyjątki zgłaszane przez operację.

Jeśli jednak piszę silnik przetwarzania, prawdopodobnie potraktuję wyjątek XPath (zaznaczony) w ten sam sposób, co NPE: Pozwoliłbym mu przejść na szczyt wątku roboczego, pomiń resztę tej partii, zaloguj się problem (lub wyślij go do działu pomocy technicznej w celu diagnozy) i zostaw opinię użytkownikowi, aby skontaktował się z pomocą techniczną.

Dave Elton
źródło
2
Dokładnie. Łatwa i bezpośrednia, taka jak obsługa wyjątków. Jak mówi Dave, poprawna obsługa wyjątków odbywa się zwykle na wysokim poziomie . „Rzuć wcześnie, złap późno” to zasada. Sprawdzone wyjątki utrudniają to.
Thomas W
4

Ten artykuł jest najlepszym tekstem na temat obsługi wyjątków w Javie, jaki kiedykolwiek czytałem.

Opowiada się za niesprawdzonymi sprawdzonymi wyjątkami, ale wybór ten jest bardzo dokładnie wyjaśniony i oparty na mocnych argumentach.

Nie chcę tutaj przytaczać zbyt wiele treści artykułu (najlepiej czytać go jako całość), ale obejmuje on większość argumentów niezaznaczonych zwolenników wyjątków z tego wątku. Szczególnie omawiany jest ten argument (który wydaje się dość popularny):

Weźmy przypadek, gdy wyjątek został zgłoszony gdzieś u dołu warstw API i po prostu pojawił się bąbelek, ponieważ nikt nie wiedział, że może nawet wystąpić ten błąd, mimo że był to rodzaj błędu, który był bardzo prawdopodobny, gdy kod wywołujący wyrzuciłem go (na przykład FileNotFoundException w przeciwieństwie do VogonsTrashingEarthExcept ... w takim przypadku nie miałoby znaczenia, czy będziemy to robić, czy nie, ponieważ nie ma już nic do obsługi).

Autor „odpowiedzi”:

Absolutnie niepoprawne jest założenie, że wszystkie wyjątki środowiska wykonawczego nie powinny być wychwytywane i zezwalać na propagację na samą „górę” aplikacji. (...) Dla każdego wyjątkowego warunku, który musi być wyraźnie obsłużony - przez wymagania systemowe / biznesowe - programiści muszą zdecydować, gdzie go złapać i co zrobić, gdy warunek zostanie złapany. Należy to zrobić ściśle zgodnie z rzeczywistymi potrzebami aplikacji, a nie na podstawie alertu kompilatora. Wszystkie inne błędy muszą mieć możliwość swobodnego rozprzestrzeniania się do najwyższego modułu obsługi, w którym zostaną zarejestrowane, i zostanie podjęta wdzięczna (być może, zakończenie) akcja.

A główną myślą lub artykułem jest:

Jeśli chodzi o obsługę błędów w oprogramowaniu, jedynym bezpiecznym i poprawnym założeniem, jakie można kiedykolwiek przyjąć, jest to, że awaria może wystąpić absolutnie w każdym podprogramie lub module, który istnieje!

Jeśli więc „ nikt nie wiedział, że wystąpił ten błąd ”, coś jest nie tak z tym projektem. Taki wyjątek powinien być obsługiwany przez przynajmniej najbardziej ogólną procedurę obsługi wyjątków (np. Taką, która obsługuje wszystkie wyjątki nieobsługiwane przez bardziej szczegółowe procedury obsługi), jak sugeruje autor.

Tak smutne, że niewielu ludzi odkrywa ten wspaniały artykuł :-(. Z całego serca polecam każdemu, kto się waha, które podejście lepiej poświęcić trochę czasu i go przeczytać.

Piotr Sobczyk
źródło
4

Sprawdzone wyjątki były, w pierwotnej formie, raczej próbą radzenia sobie z przypadkami niż z awariami. Chwalącym celem było wyróżnienie określonych przewidywalnych punktów (niemożność połączenia, nie znaleziono pliku itp.) I zapewnienie, że programiści sobie z nimi poradzą.

To, co nigdy nie było zawarte w pierwotnej koncepcji, polegało na wymuszeniu szerokiej gamy systemowych i niemożliwych do naprawienia awarii, które należy zadeklarować. Te awarie nigdy nie były poprawne, aby mogły zostać zadeklarowane jako sprawdzone wyjątki.

Błędy są generalnie możliwe w kodzie, a kontenery EJB, web & Swing / AWT już to obsługują, zapewniając najbardziej zewnętrzny program obsługi wyjątków „nieudane żądanie”. Najbardziej podstawową prawidłową strategią jest wycofanie transakcji i zwrócenie błędu.

Jednym z kluczowych punktów jest to, że środowisko wykonawcze i sprawdzone wyjątki są funkcjonalnie równoważne.Nie ma obsługi ani odzyskiwania, które sprawdzałyby wyjątki, czego nie mogą zrobić wyjątki środowiska wykonawczego.

Największym argumentem przeciwko „sprawdzonym” wyjątkom jest to, że większości wyjątków nie można naprawić. Prostym faktem jest to, że nie jesteśmy właścicielami kodu / podsystemu, który się zepsuł. Nie widzimy implementacji, nie jesteśmy za nią odpowiedzialni i nie możemy tego naprawić.

Jeśli nasza aplikacja nie jest DB, nie powinniśmy próbować naprawiać DB. Naruszyłoby to zasadę hermetyzacji .

Szczególnie problematyczne były obszary JDBC (SQLException) i RMI dla EJB (RemoteException). Zamiast identyfikowania możliwych do naprawienia nieprzewidzianych sytuacji zgodnie z pierwotną koncepcją „sprawdzonego wyjątku”, wymuszono powszechne zgłaszanie powszechnych problemów z niezawodnością systemową, których nie da się naprawić.

Inną poważną wadą w projekcie Java było to, że obsługa wyjątków powinna być poprawnie umieszczona na najwyższym możliwym poziomie „biznesowym” lub „żądanie”. Zasada jest taka: „rzucaj wcześnie, łapaj późno”. Sprawdzone wyjątki niewiele dają, ale przeszkadzają.

W Javie mamy oczywisty problem z wymaganiem tysięcy bloków typu try-catch typu „nic nie rób”, przy czym znaczna część (40% +) jest błędnie kodowana. Prawie żadne z nich nie implementuje żadnej autentycznej obsługi ani niezawodności, ale nakłada znaczne koszty kodowania.

Wreszcie „sprawdzone wyjątki” są prawie niezgodne z programowaniem funkcjonalnym FP.

Ich nacisk na „natychmiastowe postępowanie” stoi w sprzeczności zarówno z najlepszymi praktykami „wychwytywania opóźnień” w wyjątkach, jak i dowolną strukturą FP, która wyodrębnia pętle / przepływ kontroli.

Wiele osób mówi o „obsłudze” sprawdzonych wyjątków, ale rozmawia przez czapki. Kontynuowanie po niepowodzeniu z zerowymi, niekompletnymi lub niepoprawnymi danymi w celu udawania sukcesu nie jest niczym. To błąd w inżynierii / niezawodności najniższej formy.

Niepowodzenie czysto jest najbardziej podstawową poprawną strategią obsługi wyjątku. Wycofywanie transakcji, rejestrowanie błędu i zgłaszanie użytkownikowi odpowiedzi „niepowodzenie” to dobra praktyka - a co najważniejsze, zapobieganie przypisywaniu nieprawidłowych danych biznesowych do bazy danych.

Inne strategie obsługi wyjątków to „ponów”, „ponownie połącz” lub „pomiń” na poziomie biznesowym, podsystemu lub żądania. Wszystkie są ogólnymi strategiami niezawodności i działają dobrze / lepiej z wyjątkami środowiska wykonawczego.

Wreszcie zdecydowanie lepiej jest zawieść, niż uruchamiać z niepoprawnymi danymi. Kontynuowanie spowoduje albo błędy wtórne, odległe od pierwotnej przyczyny i trudniejsze do debugowania; lub ostatecznie spowoduje popełnienie błędnych danych. Ludzie zostają do tego zwolnieni.

Zobacz:
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/

Thomas W.
źródło
1
Chodzi mi o to, by nie udać się właściwie jako ogólnej strategii. Niezaznaczone wyjątki pomagają, ponieważ nie wymuszają wprowadzania bloków catch. Przechwytywanie i rejestrowanie błędów można następnie pozostawić kilku zewnętrznym programom obsługi, zamiast błędnie kodować tysiące razy w całej bazie kodu (co w rzeczywistości kryje błędy) . W przypadku dowolnych awarii niesprawdzone wyjątki są absolutnie najbardziej poprawne. Nieprzewidziane zdarzenia - przewidywalne wyniki, takie jak niewystarczające fundusze - to jedyne wyjątki, które zgodnie z prawem warto sprawdzić.
Thomas W
1
Moja odpowiedź już powyżej dotyczy tego. Przede wszystkim: 1) Zewnętrzny moduł obsługi awarii powinien wszystko złapać. Poza tym, tylko dla określonych zidentyfikowanych miejsc, 2) Można wychwycić i obsłużyć określone spodziewane zdarzenia awaryjne - w bezpośrednim miejscu, w którym są wyrzucane. Oznacza to, że nie znaleziono pliku, niewystarczające fundusze itp. W momencie, w którym można je odzyskać - nie więcej. Zasada enkapsulacji oznacza, że ​​zewnętrzne warstwy nie mogą / nie powinny być odpowiedzialne za zrozumienie / odzyskanie po awarii głęboko w środku. Po trzecie, 3) Wszystko inne należy wyrzucić na zewnątrz - jeśli to możliwe, odznaczone.
Thomas W
1
Najbardziej zewnętrzny moduł obsługi przechwytuje wyjątek, rejestruje go i zwraca odpowiedź „nieudaną” lub wyświetla okno dialogowe błędu. Bardzo proste, wcale nie trudne do zdefiniowania. Chodzi o to, że każdy wyjątek, który nie jest natychmiast i lokalnie możliwy do odzyskania, jest niepowodzeniem , którego można odzyskać z powodu zasady enkapsulacji. Jeśli kod, o którym należy wiedzieć, nie może go odzyskać, ogólne żądanie kończy się niepowodzeniem w sposób czysty i prawidłowy. To jest właściwy sposób, aby zrobić to poprawnie.
Thomas W
1
Błędny. Zadaniem najbardziej zewnętrznego modułu obsługi jest niepowodzenie w czystym działaniu i rejestrowanie błędów na granicy „żądania”. Złamane żądanie kończy się niepowodzeniem, zgłaszany jest wyjątek, wątek może kontynuować obsługę następnego żądania. Taki najbardziej zewnętrzny moduł obsługi jest standardową funkcją w kontenerach Tomcat, AWT, Spring, EJB i „głównym” wątku Java.
Thomas W
1
Dlaczego zgłaszanie „prawdziwych błędów” na granicy żądania lub w najbardziej zewnętrznym module obsługi jest niebezpieczne? Często pracuję nad integracją systemów i niezawodnością, gdzie właściwie ważna jest poprawna inżynieria niezawodności i do tego używam podejść „niesprawdzonych wyjątków”. Nie jestem do końca pewien, o czym tak naprawdę debatujesz - wydaje się, że mógłbyś chcieć spędzić 3 miesiące w niesprawdzony wyjątkowy sposób, poczuć to, a może być może będziemy mogli dalej dyskutować. Dzięki.
Thomas W,
4

Jak już powiedzieli ludzie, sprawdzone wyjątki nie istnieją w kodzie bajtowym Java. Są po prostu mechanizmem kompilatora, podobnie jak inne kontrole składniowe. Widzę sprawdzane wyjątki dużo jak widzę kompilator narzeka zbędne warunkowe: if(true) { a; } b;. Jest to pomocne, ale mogłem to zrobić celowo, więc pozwól mi zignorować twoje ostrzeżenia.

Faktem jest, że nie będziesz w stanie zmusić każdego programisty do „robienia właściwej rzeczy”, jeśli egzekwujesz sprawdzone wyjątki, a wszyscy inni są teraz szkodami ubocznymi, którzy po prostu nienawidzą cię za regułę, którą podjąłeś.

Napraw złe programy tam! Nie próbuj poprawiać języka, aby im na to nie pozwolić! Dla większości ludzi „robienie czegoś z wyjątkiem” to tak naprawdę po prostu mówienie o tym użytkownikowi. Mogę również poinformować użytkownika o niezaznaczonym wyjątku, więc trzymaj sprawdzone klasy wyjątków poza moim API.

Jaskółka oknówka
źródło
Właśnie, chciałem tylko podkreślić różnicę między nieosiągalnym kodem (który generuje błąd) a warunkowymi z przewidywalnym wynikiem. Usunę ten komentarz później.
Holger
3

Problem ze sprawdzonymi wyjątkami polega na tym, że wyjątki są często dołączane do metod interfejsu, jeśli używa go nawet jedna implementacja tego interfejsu.

Innym problemem związanym ze sprawdzonymi wyjątkami jest to, że są one niewłaściwie wykorzystywane. Idealnym przykładem jest java.sql.Connection„s close()metody. Może rzucić SQLException, nawet jeśli już wyraźnie powiedziałeś , że skończyłeś z Connection. Jakie informacje można zamknąć (), które mogą Cię zainteresować?

Zwykle kiedy zamykam () połączenie *, wygląda to tak:

try {
    conn.close();
} catch (SQLException ex) {
    // Do nothing
}

Nie zaczynaj też od różnych metod analizy i wyjątku NumberFormatException ... TryParse .NET, który nie rzuca wyjątków, jest o wiele łatwiejszy w użyciu, bolesne jest powrót do Java (używamy zarówno Java, jak i C # gdzie pracuję).

*Jako dodatkowy komentarz, Connection.close () z PooledConnection nawet nie zamyka połączenia, ale nadal musisz wychwycić wyjątek SQLEx, ponieważ jest to sprawdzony wyjątek.

Władca
źródło
2
Tak, każdy ze sterowników może… pytanie brzmi raczej „dlaczego programista powinien się tym przejmować?” ponieważ i tak ma dostęp do bazy danych. Dokumenty ostrzegają cię nawet, że zawsze powinieneś zatwierdzić () lub wycofać () bieżącą transakcję przed wywołaniem close ().
Władca
Wiele osób uważa, że ​​zamknięcie pliku nie może wyrzucić wyjątku ... stackoverflow.com/questions/588546/ ... czy jesteś w 100% pewien, że nie ma przypadków, które miałyby znaczenie?
TofuBeer
Jestem w 100% pewien, że nie ma przypadków, które miałyby znaczenie i że dzwoniący nie spróbowałby / złapał.
Martin
1
Doskonały przykład z zamykającymi połączeniami, Martin! Mogę cię tylko sformułować: jeśli po prostu wyraźnie stwierdziliśmy, że skończyliśmy z połączeniem, po co zawracać sobie głowę tym, co się dzieje, gdy go zamykamy. Jest więcej takich przypadków, że programista tak naprawdę nie przejmuje się wystąpieniem wyjątku i ma absolutną rację.
Piotr Sobczyk
1
@PiotrSobczyk: Niektóre sterowniki SQL skwierczą, jeśli ktoś zamknie połączenie po rozpoczęciu transakcji, ale nie potwierdzi jej ani nie cofnie. IMHO, skrzeczenie jest lepsze niż ciche ignorowanie problemu, przynajmniej w przypadkach, gdy skrzeczenie nie spowoduje zgubienia innych wyjątków.
supercat
3

Programista musi znać wszystkie wyjątki, które metoda może zgłosić, aby móc z niej poprawnie korzystać. Tak więc pokonanie go nad głową z kilkoma wyjątkami niekoniecznie pomaga nieostrożnemu programistowi uniknąć błędów.

Zaletą niewielkiej korzyści jest uciążliwy koszt (szczególnie w przypadku większych, mniej elastycznych baz kodu, w których ciągłe modyfikowanie podpisów interfejsu nie jest praktyczne).

Analiza statyczna może być przyjemna, ale prawdziwie niezawodna analiza statyczna często sztywno wymaga od programisty ścisłej pracy. Istnieje kalkulacja kosztów i korzyści, a pasek musi być ustawiony wysoko dla kontroli, która prowadzi do błędu czasu kompilacji. Byłoby bardziej pomocne, gdyby IDE przyjęło rolę komunikowania, które wyjątki może zgłosić metoda (w tym które są nieuniknione). Chociaż być może nie byłby tak niezawodny bez wymuszonych deklaracji wyjątków, większość wyjątków byłaby nadal deklarowana w dokumentacji, a wiarygodność ostrzeżenia IDE nie jest tak istotna.

Aleksandr Dubinsky
źródło
2

Myślę, że jest to doskonałe pytanie i wcale nie jest argumentacyjne. Myślę, że biblioteki stron trzecich powinny (generalnie) zgłaszać niesprawdzone wyjątki. Oznacza to, że możesz odizolować swoje zależności od biblioteki (tj. Nie musisz ponownie rzucać ich wyjątkami ani rzucać Exception- zwykle zła praktyka). Wiosenna warstwa DAO jest tego doskonałym przykładem.

Z drugiej strony, wyjątki od podstawowego API Java powinny być ogólnie sprawdzane, czy można je kiedykolwiek obsłużyć. Weź FileNotFoundExceptionlub (mój ulubiony) InterruptedException. Warunki te prawie zawsze należy traktować konkretnie (tzn. Reakcja na anę InterruptedExceptionnie jest taka sama jak reakcja na an IllegalArgumentException). Fakt sprawdzania wyjątków zmusza programistów do zastanowienia się, czy dany warunek jest w stanie obsłużyć, czy nie. (To powiedziawszy, rzadko widywano InterruptedExceptionsię odpowiednio!)

Jeszcze jedno - a RuntimeExceptionnie zawsze „tam, gdzie deweloper coś źle zrozumiał”. Zgłaszany jest niedozwolony wyjątek argumentu podczas próby utworzenia enumużycia valueOfi nie ma enumtakiej nazwy. To niekoniecznie pomyłka dewelopera!

oxbow_lakes
źródło
1
Tak, to błąd programisty. Najwyraźniej nie użyli właściwej nazwy, więc muszą wrócić i naprawić swój kod.
AxiomaticNexus
@AxiomaticNexus Żaden rozsądny programista nie używa enumnazw członków, tylko dlatego, że enumzamiast tego używają obiektów. Zła nazwa może więc pochodzić tylko z zewnątrz, niezależnie od tego, czy jest to plik importu, czy cokolwiek innego. Jednym z możliwych sposobów radzenia sobie z takimi nazwami jest dzwonienie MyEnum#valueOfi łapanie IAE. Innym sposobem jest użycie wstępnie wypełnionego Map<String, MyEnum>, ale są to szczegóły implementacji.
maaartinus,
@maaartinus Istnieją przypadki, w których nazwy członków enum są używane bez ciągu pochodzącego z zewnątrz. Na przykład, jeśli chcesz dynamicznie zapętlać wszystkich członków, aby coś z nimi zrobić. Ponadto nie ma znaczenia, czy ciąg pochodzi z zewnątrz, czy nie. Deweloper posiada wszystkie potrzebne informacje, aby wiedzieć, czy przekazanie ciągu x do „MyEnum # valueOf” spowoduje błąd przed przekazaniem go. Przekazywanie ciągu x do „MyEnum # valueOf” w każdym razie, gdy spowodowałoby to błąd, byłoby oczywistym błędem ze strony dewelopera.
AxiomaticNexus,
2

Oto jeden argument przeciwko sprawdzonym wyjątkom (z joelonsoftware.com):

Powodem jest to, że uważam wyjątki za nie lepsze niż „goto”, uważane za szkodliwe od lat 60. XX wieku, ponieważ powodują one gwałtowny skok z jednego punktu kodu do drugiego. W rzeczywistości są znacznie gorsze niż goto:

  • Są niewidoczne w kodzie źródłowym. Patrząc na blok kodu, w tym funkcje, które mogą generować wyjątki, ale nie ma sposobu, aby zobaczyć, które wyjątki mogą zostać zgłoszone i skąd. Oznacza to, że nawet staranna kontrola kodu nie ujawnia potencjalnych błędów.
  • Tworzą zbyt wiele możliwych punktów wyjścia dla funkcji. Aby napisać poprawny kod, naprawdę musisz pomyśleć o każdej możliwej ścieżce kodu w swojej funkcji. Za każdym razem, gdy wywołujesz funkcję, która może zgłosić wyjątek i nie złapać go na miejscu, tworzysz okazje do niespodziewanych błędów spowodowanych przez funkcje, które nagle się zakończyły, pozostawiając dane w niespójnym stanie lub inne ścieżki kodu, których nie myśleć o.
finnw
źródło
3
+1 Możesz jednak podsumować argument w swojej odpowiedzi? Są jak niewidoczne gotówki i wczesne wyjścia dla twoich procedur, rozrzucone po całym programie.
MarkJ
13
To bardziej argument przeciwko wyjątkom w ogóle.
Ionuț G. Stan
10
czy rzeczywiście przeczytałeś artykuł !! Po pierwsze, mówi ogólnie o wyjątkach, po drugie sekcja „Są niewidoczne w kodzie źródłowym” dotyczy konkretnie wyjątku UNCHECKED. To jest cały sens sprawdzonej wyjątku ... tak, że wiesz, co kod rzuca jakie gdzie
Newtopian
2
@Eva Nie są takie same. Z instrukcją goto możesz zobaczyć gotosłowo kluczowe. Za pomocą pętli można zobaczyć nawias zamykający breaklub continuesłowo kluczowe lub . Wszyscy przeskakują do punktu w bieżącej metodzie. Ale nie zawsze można to zobaczyć throw, ponieważ często nie jest to w obecnej metodzie, ale w innej metodzie, którą wywołuje (być może pośrednio)
finnw
5
@finnw Funkcje same w sobie są formą goto. Zwykle nie wiesz, jakie funkcje wywołują funkcje, które wywołujesz. Jeśli programowałeś bez funkcji, nie miałbyś problemu z niewidocznymi wyjątkami. Co oznacza, że ​​problem nie jest konkretnie związany z wyjątkami i nie jest ważnym argumentem przeciwko wyjątkom w ogóle. Można powiedzieć, że kody błędów są szybsze, można powiedzieć, że monady są czystsze, ale argument goto jest głupi.
Eva,
2

Dobrym dowodem na to, że sprawdzone wyjątki nie są potrzebne są:

  1. Dużo frameworka, który działa w Javie. Podobnie jak Spring, która otacza wyjątek JDBC niezaznaczonymi wyjątkami, wyrzucając komunikaty do dziennika
  2. Wiele języków, które pojawiły się po Java, nawet na platformie Java - nie używają ich
  3. Sprawdzone wyjątki, to miłe przewidywanie, w jaki sposób klient użyje kodu, który zgłasza wyjątek. Ale programista, który pisze ten kod, nigdy nie wiedziałby o systemie i biznesie, w którym pracuje klient kodu. Na przykład metody interfejsu, które zmuszają do rzucenia sprawdzonego wyjątku. W systemie jest 100 implementacji, 50 lub nawet 90 implementacji nie zgłasza tego wyjątku, ale klient nadal musi wychwycić ten wyjątek, jeśli odwołuje się do tego interfejsu. Te 50 lub 90 implementacji mają tendencję do radzenia sobie z wyjątkami wewnątrz siebie, stawiając wyjątek w logu (i jest to dla nich dobre zachowanie). Co powinniśmy z tym zrobić? Lepiej skorzystam z logiki w tle, która wykonałaby to zadanie - wysyłając wiadomość do dziennika. A jeśli ja, jako klient kodu, czuję, że potrzebuję poradzić sobie z wyjątkiem - zrobię to.
  4. Kolejny przykład, kiedy pracuję z I / O w Javie, zmusza mnie do sprawdzenia wszystkich wyjątków, jeśli plik nie istnieje? co powinienem z tym zrobić? Jeśli nie istnieje, system nie przejdzie do następnego kroku. Klient tej metody nie uzyskałby oczekiwanej zawartości z tego pliku - może obsłużyć wyjątek Runtime, w przeciwnym razie powinienem najpierw sprawdzić sprawdzony wyjątek, umieścić komunikat w dzienniku, a następnie wyrzucić wyjątek z metody. Nie ... nie - lepiej zrobiłbym to automatycznie za pomocą RuntimeEception, który robi to / lits automatycznie. Nie ma sensu obsługiwać go ręcznie - byłbym szczęśliwy widząc komunikat o błędzie w dzienniku (AOP może w tym pomóc ... coś, co naprawia java). Jeśli w końcu okaże się, że system powinien wyświetlać komunikat końcowy użytkownikowi końcowemu - pokażę to, a nie problem.

Byłem szczęśliwy, jeśli Java zapewni mi wybór , którego mam użyć podczas pracy z podstawowymi bibliotekami lib, takimi jak I / O. Like udostępnia dwie kopie tych samych klas - jedna zapakowana w RuntimeEception. Następnie możemy porównać, z czego korzystaliby ludzie . Na razie jednak wiele osób lepiej byłoby wybrać jakieś frameworki na java lub inny język. Jak Scala, JRuby cokolwiek. Wielu uważa, że ​​SUN miał rację.

ses
źródło
1
Zamiast mieć dwie wersje klas, powinien istnieć zwięzły sposób określania, że ​​żadne z wywołań metod wykonanych przez blok kodu nie powinno generować wyjątków niektórych typów, a wszelkie takie wyjątki powinny być opakowane za pomocą określonych środków i rethrown (domyślnie utwórz nowy RuntimeExceptionz odpowiednim wewnętrznym wyjątkiem). To niefortunne, że bardziej zwięzłe jest, aby metoda zewnętrzna throwsstanowiła wyjątek od metody wewnętrznej, niż owijać wyjątki od metody wewnętrznej, gdy ten drugi sposób działania byłby częściej prawidłowy.
supercat
2

Jedną ważną rzeczą, o której nikt nie wspominał, jest to, w jaki sposób zakłóca interfejsy i wyrażenia lambda.

Powiedzmy, że definiujesz MyAppException extends Exception. Jest to wyjątek najwyższego poziomu dziedziczony przez wszystkie wyjątki zgłoszone przez aplikację. Każda metoda deklaruje, że throws MyAppExceptionjest to trochę niuansowe, ale możliwe do opanowania. Procedura obsługi wyjątków rejestruje wyjątek i w jakiś sposób powiadamia użytkownika.

Wszystko wygląda OK, dopóki nie chcesz wdrożyć interfejsu, który nie jest twój. Oczywiście nie deklaruje zamiaru rzucenia MyApException, więc kompilator nie pozwala na rzucenie stamtąd wyjątku.

Jeśli jednak twój wyjątek się rozszerzy RuntimeException, nie będzie problemu z interfejsami. Możesz dobrowolnie wspomnieć o wyjątku w JavaDoc, jeśli chcesz. Ale poza tym po prostu bezgłośnie przepuszcza wszystko, aby zostać złapanym w warstwę obsługi wyjątków.

Vlasec
źródło
1

Widzieliśmy kilka odniesień do głównego architekta C #.

Oto alternatywny punkt widzenia faceta Javy, kiedy należy stosować sprawdzone wyjątki. Uznaje wiele negatywnych stron, o których wspominali inni: skuteczne wyjątki

Jacob Toronto
źródło
2
Problem ze sprawdzonymi wyjątkami w Javie wynika z głębszego problemu, który polega na tym, że zbyt wiele informacji jest zawartych w TYPIE wyjątku, a nie we właściwościach instancji. Przydałoby się sprawdzanie wyjątków, jeśli „sprawdzanie” było atrybutem stron do rzucania / łapania i jeśli można deklaratywnie określić, czy sprawdzany wyjątek, który ucieka z bloku kodu, powinien pozostać sprawdzonym wyjątkiem, czy też powinien być widoczny przez dowolny otaczający blok jako niesprawdzony wyjątek; podobnie bloki catch powinny móc określić, że chcą tylko sprawdzonych wyjątków.
supercat
1
Załóżmy, że podana jest procedura wyszukiwania słownika, która generuje pewien szczególny typ wyjątku, jeśli podjęta zostanie próba uzyskania dostępu do nieistniejącego klucza. Kod klienta może wychwycić taki wyjątek. Jeśli jednak jakaś metoda używana przez procedurę wyszukiwania wyrzuci ten sam typ wyjątku w sposób, którego nie oczekuje procedura wyszukiwania, kod klienta prawdopodobnie nie powinien go złapać. Po sprawdzeniu-Ness być nieruchomość wyjątku przypadkach , stron rzucać, i miejsc połowów, by uniknąć takich problemów. Klient wychwytuje „sprawdzone” wyjątki tego typu, unikając nieoczekiwanych.
supercat
0

Dużo czytałem o obsłudze wyjątków, nawet jeśli (przez większość czasu) naprawdę nie mogę powiedzieć, że jestem szczęśliwy lub smutny z powodu istnienia sprawdzonych wyjątków, oto moje zdanie: sprawdzone wyjątki w kodzie niskiego poziomu (IO, networking) , OS itp.) Oraz odznaczone wyjątki w interfejsach API wysokiego poziomu / poziomie aplikacji.

Nawet jeśli nie jest tak łatwo narysować linię między nimi, uważam, że naprawdę denerwujące / trudne jest zintegrowanie kilku interfejsów API / bibliotek pod tym samym dachem bez owijania przez cały czas wiele sprawdzonych wyjątków, ale z drugiej strony, czasem jest użyteczny / lepiej zostać zmuszonym do złapania jakiegoś wyjątku i podania innego, który ma większy sens w obecnym kontekście.

Projekt, nad którym pracuję, bierze wiele bibliotek i integruje je pod tym samym API, API, które jest całkowicie oparte na niesprawdzonych wyjątkach. Te ramy zapewniają API wysokiego poziomu, które na początku było pełne sprawdzonych wyjątków i miało tylko kilka niesprawdzonych wyjątki (wyjątek inicjalizacji, wyjątek konfiguracji itp.) i muszę powiedzieć, że nie był zbyt przyjazny . Przez większość czasu musiałeś wychwytywać lub ponownie rzucać wyjątki, których nie wiesz, jak sobie z nimi poradzić, lub nawet ci to nie przeszkadza (nie mylić z tobą, należy zignorować wyjątki), szczególnie po stronie klienta, gdzie jeden kliknięcie może wygenerować 10 możliwych (zaznaczonych) wyjątków.

Obecna wersja (trzecia) korzysta tylko z niezaznaczonych wyjątków i ma globalny moduł obsługi wyjątków, który jest odpowiedzialny za obsługę wszystkiego, czego nie wykryto. Interfejs API umożliwia rejestrowanie procedur obsługi wyjątków, które decydują, czy wyjątek jest uważany za błąd (przez większość czasu tak jest), co oznacza rejestrowanie i powiadamianie kogoś lub może oznaczać coś innego - na przykład wyjątek AbortException, co oznacza zerwanie bieżącego wątku wykonania i nie rejestrowanie żadnego błędu, ponieważ jest pożądane, aby tego nie robić. Oczywiście, aby wypracować, wszystkie niestandardowe wątki muszą obsługiwać metodę run () za pomocą try {...} catch (all).

public void run () {

try {
     ... do something ...
} catch (Throwable throwable) {
     ApplicationContext.getExceptionService().handleException("Handle this exception", throwable);
}

}

Nie jest to konieczne, jeśli używasz WorkerService do planowania zadań (Runnable, Callable, Worker), który obsługuje wszystko za Ciebie.

Oczywiście to tylko moja opinia i może nie być właściwa, ale wygląda na dobre podejście do mnie. Zobaczę, kiedy wydam projekt, jeśli to, co moim zdaniem jest dobre dla mnie, jest również dobre dla innych ... :)

adrian.tarau
źródło