Sprawdzone kontra niesprawdzone vs Bez wyjątku… Najlepsza praktyka przeciwnych przekonań


10

Istnieje wiele wymagań, aby system mógł poprawnie przekazywać i obsługiwać wyjątki. Istnieje również wiele opcji wyboru języka do wdrożenia tej koncepcji.

Wymagania dotyczące wyjątków (w określonej kolejności):

  1. Dokumentacja : język powinien umożliwiać dokumentowanie wyjątków, które API może zgłaszać. Idealnie, ten nośnik dokumentacji powinien nadawać się do użytku maszynowego, aby umożliwić kompilatorom i IDE wsparcie programistom.

  2. Przekazywanie wyjątkowych sytuacji : ta jest oczywista, aby umożliwić funkcji przekazywanie sytuacji, które uniemożliwiają wywołanej funkcji wykonanie oczekiwanej akcji. Moim zdaniem istnieją trzy duże kategorie takich sytuacji:

    2.1 Błędy w kodzie, które powodują, że niektóre dane są nieprawidłowe.

    2.2 Problemy z konfiguracją lub innymi zasobami zewnętrznymi.

    2.3 Zasoby, które są z natury zawodne (sieć, systemy plików, bazy danych, użytkownicy końcowi itp.). Są to trochę zasadne przypadki, ponieważ ich niewiarygodna natura powinna sprawić, że spodziewamy się ich sporadycznych niepowodzeń. Czy w takim przypadku sytuacje te należy uznać za wyjątkowe?

  3. Podaj wystarczającą ilość informacji, aby kod mógł go obsłużyć : wyjątki powinny zapewniać odbiorcy wystarczające informacje, aby mógł zareagować i ewentualnie poradzić sobie z sytuacją. informacje powinny również być wystarczające, aby po zalogowaniu wyjątki zapewniały programiście wystarczający kontekst do zidentyfikowania i wyodrębnienia złych stwierdzeń i zapewnienia rozwiązania.

  4. Zapewnij programistom pewność co do aktualnego stanu stanu wykonania jego kodu : Możliwości obsługi wyjątków w systemie oprogramowania powinny być na tyle obecne, aby zapewnić niezbędne zabezpieczenia, jednocześnie nie wchodząc w drogę programistom, aby mógł skupić się na zadaniu dłoń.

Aby uwzględnić te, w różnych językach wdrożono następujące metody:

  1. Sprawdzone wyjątki Zapewniają świetny sposób na dokumentowanie wyjątków, a teoretycznie, jeśli są poprawnie wdrożone, powinny zapewnić wystarczającą pewność, że wszystko jest w porządku. Jednak koszt jest taki, że wielu uważa, że ​​bardziej produktywne jest po prostu ominięcie albo przez połknięcie wyjątków, albo ponowne ich odrzucenie jako niesprawdzone wyjątki. W przypadku niewłaściwie sprawdzonego wyjątku traci on całą swoją użyteczność. Sprawdzone wyjątki utrudniają utworzenie stabilnego w czasie interfejsu API. Implementacje ogólnego systemu w ramach konkretnej domeny przyniosą mnóstwo wyjątkowej sytuacji, która byłaby trudna do utrzymania przy użyciu wyłącznie sprawdzonych wyjątków.

  2. Niesprawdzone wyjątki - znacznie bardziej wszechstronne niż sprawdzone wyjątki, nie udokumentują prawidłowo możliwych wyjątkowych sytuacji w danej implementacji. Opierają się na dokumentacji ad hoc, jeśli w ogóle. Stwarza to sytuacje, w których zawodna natura medium jest maskowana przez interfejs API, który daje wrażenie niezawodności. Również po rzuceniu wyjątki te tracą znaczenie, gdy wracają w górę przez warstwy abstrakcji. Ponieważ są słabo udokumentowane, programista nie może specjalnie zaatakować ich i często musi rzucić znacznie szerszą sieć niż jest to konieczne, aby zapewnić, że systemy wtórne, w przypadku awarii, nie spowodują awarii całego systemu. Co prowadzi nas z powrotem do problemu z połykaniem, sprawdzone wyjątki.

  3. Typy zwrotów wielostanowiskowych W tym przypadku należy polegać na rozłącznym zestawie, krotce lub innej podobnej koncepcji, aby zwrócić oczekiwany wynik lub obiekt reprezentujący wyjątek. Tutaj nie ma rozwijania stosu, bez przecinania kodu, wszystko działa normalnie, ale wartość zwracana musi być sprawdzona pod kątem błędu przed kontynuowaniem. Tak naprawdę nie pracowałem z tym jeszcze, więc nie mogę komentować z doświadczenia. Przyznaję, że rozwiązuje to pewne wyjątki od omijania normalnego przepływu, ale nadal będzie cierpiał z powodu tych samych problemów, co sprawdzone wyjątki, jako męczących i stale „na twarz”.

Pytanie brzmi:

Jakie masz doświadczenie w tej sprawie i co według ciebie jest najlepszym kandydatem do stworzenia dobrego systemu obsługi wyjątków dla danego języka?


EDYCJA: Kilka minut po napisaniu tego pytania natknąłem się na ten post , straszne!


2
„będzie cierpieć z powodu tych samych problemów, co sprawdzone wyjątki, jako męczących i ciągle w twarz”: Niezupełnie: przy odpowiednim wsparciu językowym musisz jedynie zaprogramować „ścieżkę sukcesu”, a maszyneria językowa leżąca u podstaw zajmuje się propagowaniem błędy.
Giorgio

„Język powinien umożliwiać dokumentowanie wyjątków, które API może zgłaszać. - weeeel. W C ++ „dowiedzieliśmy się”, że to tak naprawdę nie działa. Wszystko, co może naprawdę skutecznie zrobić, to państwa, czy API może rzucić żadnego wyjątku. (To naprawdę krótka krótka historia, ale myślę, że spojrzenie na tę noexcepthistorię w C ++ może dać bardzo dobry wgląd w EH w C # i Javie.)
Martin Ba

Odpowiedzi:


10

We wczesnych latach C ++ odkryliśmy, że bez jakiegoś ogólnego programowania, mocno pisane języki były wyjątkowo niewygodne. Odkryliśmy również, że sprawdzone wyjątki i ogólne programowanie nie działały dobrze razem, a sprawdzone wyjątki zostały zasadniczo porzucone.

Typy zwrotów wielosetowych są świetne, ale nie zastępują wyjątków. Bez wyjątków kod jest pełen szumu sprawdzającego błędy.

Innym problemem związanym ze sprawdzonymi wyjątkami jest to, że zmiana wyjątków zgłaszana przez funkcję niskiego poziomu wymusza kaskadę zmian wszystkich dzwoniących i ich dzwoniących itd. Jedynym sposobem, aby temu zapobiec, jest przechwycenie wyjątków zgłaszanych przez niższe poziomy i zawinięcie ich w nowy wyjątek na każdym poziomie kodu. Ponownie otrzymujesz bardzo głośny kod.


2
Generics pomaga rozwiązać całą klasę błędów, które są głównie spowodowane ograniczeniem wsparcia języka dla paradygmatu OO. jednak alternatywą wydaje się być albo kod, który głównie sprawdza błędy, albo działa, mając nadzieję, że nic się nie powiedzie. Albo ciągle masz wyjątkowe sytuacje, albo żyjesz w krainie snów puszystych białych króliczków, które stają się brzydkie, gdy upuścisz dużego złego wilka na środek!
Newtopian

3
+1 za problem kaskadowy. Każdy system / architektura, która utrudnia zmianę, prowadzi tylko do łatania małp i nieporządnych systemów, bez względu na to, jak dobrze zaprojektowali je autorzy.
Matthieu M.,

2
@Newtopian: Szablony robią rzeczy, których nie można zrobić w ścisłej orientacji obiektowej, np. Zapewniają bezpieczeństwo typu statycznego dla ogólnych pojemników.
David Thornley

2
Chciałbym zobaczyć system wyjątków z koncepcją „sprawdzonych wyjątków”, ale bardzo różniący się od Javy. Sprawdzone-ności nie powinny być atrybutem wyjątku typu , ale wyrzucić, serwisy połowu, oraz przypadki wyjątkiem; jeśli metoda jest reklamowana jako zgłoszenie sprawdzonego wyjątku, powinno to mieć dwa efekty: (1) funkcja powinna obsłużyć „rzut” sprawdzonego wyjątku, wykonując coś specjalnego po powrocie (np. ustawiając flagę carry itp. w zależności od dokładna platforma), na który kod telefoniczny byłby przygotowany.
supercat

7
„Bez wyjątków kod jest pełen szumu sprawdzającego błędy.”: Nie jestem tego pewien: w Haskell możesz do tego używać monad i wszystkie szumy sprawdzania błędów zniknęły. Hałas wprowadzany przez „wielostanowiskowe typy zwrotów” jest bardziej ograniczeniem języka programowania niż samego rozwiązania.
Giorgio

9

Przez długi czas języki OO stosowanie wyjątków było de facto standardem w komunikowaniu błędów. Ale funkcjonalne języki programowania dają możliwość innego podejścia, np. Przy użyciu monad (których nie używałem) lub bardziej lekkiego „programowania zorientowanego na kolej”, jak opisał Scott Wlaschin.

To naprawdę wariant wielostanowiskowego typu wyniku.

  • Funkcja zwraca albo sukces, albo błąd. Nie może zwrócić obu (jak w przypadku krotki).
  • Wszystkie możliwe błędy zostały zwięźle udokumentowane (przynajmniej w F # z typami wyników jako związki dyskryminowane).
  • Dzwoniący nie może użyć wyniku bez uwzględnienia, czy wynik był sukcesem, czy porażką.

Typ wyniku można zadeklarować w ten sposób

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

Zatem wynikiem funkcji zwracającej ten typ byłby albo a, Successalbo Failtyp. Nie może to być jedno i drugie.

W bardziej imperatywnych językach programowania ten styl może wymagać dużej ilości kodu na stronie wywołującej. Ale programowanie funkcjonalne umożliwia konstruowanie funkcji wiązania lub operatorów w celu powiązania wielu funkcji, dzięki czemu sprawdzanie błędów nie zajmuje połowy kodu. Jako przykład:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

updateUserWywołania funkcji każdej z tych funkcji z rzędu, a każdy z nich może zakończyć się niepowodzeniem. Jeśli wszystkie się powiedzą, zwracany jest wynik ostatnio wywołanej funkcji. Jeśli jedna z funkcji zawiedzie, wynik tej funkcji będzie wynikiem ogólnej updateUserfunkcji. Wszystko to jest obsługiwane przez niestandardowy operator >> =.

W powyższym przykładzie typami błędów mogą być

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

Jeśli program wywołujący updateUsernie obsługuje jawnie wszystkich możliwych błędów funkcji, kompilator wygeneruje ostrzeżenie. Więc masz wszystko udokumentowane.

W Haskell istnieje dozapis, dzięki któremu kod może być jeszcze bardziej przejrzysty.


2
Bardzo dobra odpowiedź i referencje (programowanie zorientowane na kolej), +1. Warto wspomnieć o donotacji Haskella , która czyni kod wynikowy jeszcze czystszym.
Giorgio

1
@Giorgio - teraz pracowałem, ale nie pracowałem z Haskellem, tylko F #, więc naprawdę nie mogłem dużo o tym pisać. Ale możesz dodać do odpowiedzi, jeśli chcesz.
Pete

Dzięki, napisałem mały przykład, ale ponieważ nie był on wystarczająco mały, aby dodać go do twojej odpowiedzi, napisałem pełną odpowiedź (z dodatkowymi informacjami dodatkowymi).
Giorgio

2
To Railway Oriented Programmingjest dokładnie monadyczne zachowanie.
Daenyth,

5

Uważam, że odpowiedź Pete'a jest bardzo dobra i chciałbym dodać trochę przemyśleń i jeden przykład. Bardzo interesującą dyskusję dotyczącą zastosowania wyjątków w porównaniu ze zwracaniem specjalnych wartości błędów można znaleźć w Programowaniu w Standardowym ML, autorstwa Roberta Harpera , na końcu rozdziału 29.3, strona 243, 244.

Problem polega na implementacji funkcji częściowej fzwracającej wartość pewnego typu t. Jednym z rozwiązań jest, aby funkcja miała typ

f : ... -> t

i wrzuć wyjątek, gdy nie ma możliwego wyniku. Drugim rozwiązaniem jest zaimplementowanie funkcji z typem

f : ... -> t option

i powrócić SOME vdo sukcesu i NONEdo porażki.

Oto tekst z książki, z niewielkimi dostosowaniami dokonanymi przeze mnie, aby tekst był bardziej ogólny (książka odnosi się do konkretnego przykładu). Zmodyfikowany tekst jest pisany kursywą .

Jakie są kompromisy między tymi dwoma rozwiązaniami?

  1. Rozwiązanie oparte na typach opcji wyraźnie określa w typie funkcji fmożliwość awarii. Zmusza to programistę do jawnego testowania awarii przy użyciu analizy przypadków na wyniku wywołania. Kontroler typu upewni się, że nie można używać t optiontam, gdziet jest oczekiwany. Rozwiązanie oparte na wyjątkach nie wskazuje wprost na błąd w swoim typie. Jednak programista jest jednak zmuszony poradzić sobie z awarią, ponieważ w przeciwnym razie w czasie wykonywania, a nie w czasie kompilacji, pojawiałby się nieprzechwycony błąd wyjątku.
  2. Rozwiązanie oparte na typach opcji wymaga jednoznacznej analizy przypadku każdego wyniku. Jeśli „większość” wyników zakończy się powodzeniem, kontrola jest zbędna, a zatem nadmiernie kosztowna. Rozwiązanie oparte na wyjątkach jest wolne od tego narzutu: jest tendencyjne w stosunku do „normalnego” przypadku zwrotu t, a nie do „niepowodzenia” w przypadku braku zwrotu wyniku . Implementacja wyjątków zapewnia, że ​​użycie procedury obsługi jest bardziej wydajne niż jawna analiza przypadku, w przypadku gdy niepowodzenie jest rzadkie w porównaniu do sukcesu.

[cut] Zasadniczo, jeśli wydajność jest najważniejsza, zwykle preferujemy wyjątki, jeśli awaria jest rzadkością, i preferujemy opcje, jeśli awaria jest stosunkowo powszechna. Z drugiej strony, jeśli sprawdzanie statyczne jest najważniejsze, wówczas korzystne jest użycie opcji, ponieważ sprawdzanie typu będzie wymuszać wymóg sprawdzania przez programistę pod kątem błędów, zamiast powodowania błędu tylko w czasie wykonywania.

Dotyczy to wyboru między wyjątkami a typami zwrotu opcji.

Jeśli chodzi o pomysł, że reprezentowanie błędu w typie zwracanym prowadzi do kontroli błędów rozłożonych na cały kod: nie musi tak być. Oto mały przykład w Haskell, który to ilustruje.

Załóżmy, że chcemy przeanalizować dwie liczby, a następnie podzielić pierwszą przez drugą. Może więc wystąpić błąd podczas analizowania każdej liczby lub dzielenia (dzielenie przez zero). Musimy więc sprawdzić błąd po każdym kroku.

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n `div` d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

Analiza i podział są wykonywane w let ...bloku. Zauważ, że używając Maybemonady i donotacji, określa się tylko ścieżkę sukcesu : semantyka Maybemonady niejawnie propaguje wartość błędu ( Nothing). Bez kosztów ogólnych dla programisty.


2
Myślę, że w takich przypadkach, w których chcesz wydrukować przydatny komunikat o błędzie, ten Eithertyp byłby bardziej odpowiedni. Co robisz, jeśli Nothingtu dotrzesz ? Po prostu pojawia się komunikat „błąd”. Niezbyt przydatne do debugowania.
sara

1

Stałem się wielkim fanem sprawdzonych wyjątków i chciałbym podzielić się ogólną zasadą, kiedy z nich korzystać.

Doszedłem do wniosku, że mój kod ma zasadniczo 2 rodzaje błędów. Występują błędy, które można przetestować przed wykonaniem kodu oraz błędy, których nie można przetestować przed wykonaniem kodu. Prosty przykład błędu, który można przetestować przed wykonaniem kodu w NullPointerException.

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

Prosty test mógł uniknąć błędu, takiego jak ...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

Są chwile w obliczeniach, w których możesz wykonać 1 lub więcej testów przed wykonaniem kodu, aby upewnić się, że jesteś bezpieczny I WCIĄŻ WYSTĄPISZ WYJĄTEK. Na przykład możesz przetestować system plików, aby upewnić się, że na dysku twardym jest wystarczająca ilość miejsca przed zapisaniem danych na dysku. W wieloprocesowym systemie operacyjnym, takim jak te używane obecnie, proces może przetestować miejsce na dysku, a system plików zwróci wartość informującą o wystarczającej ilości miejsca, a następnie przełączenie kontekstu na inny proces może zapisać pozostałe bajty dostępne dla systemu operacyjnego system. Kiedy kontekst systemu operacyjnego powróci do uruchomionego procesu, w którym zapisujesz zawartość na dysk, nastąpi wyjątek po prostu dlatego, że w systemie plików nie ma wystarczającej ilości miejsca na dysku.

Uważam powyższy scenariusz za idealny przypadek sprawdzonego wyjątku. Jest to wyjątek w kodzie, który zmusza cię do radzenia sobie z czymś złym, nawet jeśli Twój kod może być doskonale napisany. Jeśli zdecydujesz się robić złe rzeczy, takie jak „połknąć wyjątek”, jesteś złym programistą. Nawiasem mówiąc, znalazłem przypadki, w których uzasadnione jest połknięcie wyjątku, ale proszę zostawić komentarz w kodzie, dlaczego wyjątek został połknięty. Nie można winić mechanizmu obsługi wyjątków. Zwykle żartuję, że wolę, aby mój rozrusznik serca był napisany w języku, który ma sprawdzone wyjątki.

Są chwile, kiedy trudno jest zdecydować, czy kod jest testowalny, czy nie. Na przykład, jeśli piszesz interpreter, a wyjątek SyntaxExp jest generowany, gdy kod nie działa z jakiegoś powodu składniowego, czy wyjątek SyntaxException powinien być sprawdzonym wyjątkiem, czy (w Javie) wyjątkiem RuntimeException? Chciałbym odpowiedzieć, jeśli interpreter sprawdzi składnię kodu przed wykonaniem kodu, wówczas wyjątek powinien być RuntimeException. Jeśli interpreter po prostu uruchamia kod „na gorąco” i po prostu napotyka błąd składniowy, powiedziałbym, że wyjątkiem powinien być wyjątek sprawdzony.

Przyznaję, że nie zawsze jestem szczęśliwy, że muszę złapać lub rzucić Checked Exception, ponieważ są chwile, w których nie jestem pewien, co robić. Sprawdzone wyjątki są sposobem zmuszenia programisty do uważania na potencjalny problem, który może wystąpić. Jednym z powodów, dla których programuję w Javie jest to, że ma sprawdzone wyjątki.


1
Wolałbym, żeby mój rozrusznik serca był napisany w języku, który w ogóle nie miał wyjątków, a wszystkie wiersze kodu obsługiwały błędy poprzez kody powrotu. Zgłaszając wyjątek, mówisz „wszystko poszło nie tak”, a jedynym bezpiecznym sposobem na kontynuowanie przetwarzania jest zatrzymanie i ponowne uruchomienie. Program, który tak łatwo kończy się nieprawidłowym stanem, nie jest czymś, czego potrzebujesz do krytycznego oprogramowania (a Java wyraźnie nie zezwala na jego użycie do krytycznego oprogramowania w
umowie

Używając wyjątku i nie sprawdzając ich w porównaniu z użyciem kodu zwrotnego i nie sprawdzając ich w końcu, wszystko to powoduje zatrzymanie akcji serca.
Newtopian

-1

Obecnie jestem w trakcie dość dużego projektu / interfejsu API opartego na OOP i użyłem tego układu wyjątków. Ale wszystko tak naprawdę zależy od tego, jak głęboko chcesz przejść z obsługą wyjątków i tym podobnymi.

ExpectedException
-
AuthorisedException
- EmptySetException
- NoRemainingException
- NoRowsException - NotFoundException
- ValidationException

U nieoczekiwany wyjątek
- ConnectivityException
- EnvironmentException
- ProgrammerException
- SQLException

PRZYKŁAD

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}

11
Jeśli spodziewany jest wyjątek, tak naprawdę nie jest on wyjątkiem. „NoRowsException”? Brzmi dla mnie jak kontrola, a zatem słabe użycie wyjątku.
quentin-starin

1
@qes: Sensowne jest zgłaszanie wyjątku, gdy funkcja nie jest w stanie obliczyć wartości, np. double Math.sqrt (double v) lub User findUser (long id). Daje to dzwoniącemu swobodę wychwytywania i obsługi błędów tam, gdzie jest to wygodne, zamiast sprawdzania po każdym połączeniu.
kevin cline

1
Oczekiwany = kontrola przepływu = anty-wzór wyjątku. Wyjątek nie powinien być wykorzystywany do sterowania przepływem. Jeśli oczekuje się, że spowoduje błąd dla określonych danych wejściowych, to po prostu zostanie przekazany część wartości zwracanej. Więc mamy NANlub NULL.
Eonil

1
@Eonil ... lub Option <T>
Maarten Bodewes
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.