Dlaczego wyjątki powinny być używane konserwatywnie?


80

Często widzę / słyszę, jak ludzie mówią, że wyjątków należy używać rzadko, ale nigdy nie wyjaśniam, dlaczego. Chociaż może to być prawda, racjonalne uzasadnienie jest zwykle proste: „nie bez powodu nazywa się to wyjątkiem”, co według mnie wydaje się być rodzajem wyjaśnienia, którego żaden szanowany programista / inżynier nigdy nie powinien zaakceptować.

Istnieje szereg problemów, które można rozwiązać za pomocą wyjątku. Dlaczego używanie ich do sterowania przepływem jest nierozsądne? Jaka jest filozofia bycia wyjątkowo konserwatywnym w sposobie ich używania? Semantyka? Wydajność? Złożoność? Estetyka? Konwencja?

Widziałem już wcześniej analizy wydajności, ale na poziomie, który byłby odpowiedni dla niektórych systemów, a nieistotny dla innych.

Ponownie, niekoniecznie nie zgadzam się z tym, że należy je zachować na szczególne okoliczności, ale zastanawiam się, jakie jest uzasadnienie konsensusu (jeśli coś takiego istnieje).



32
To nie jest duplikat. Połączony przykład dotyczy tego, czy obsługa wyjątków jest w ogóle przydatna. Chodzi o rozróżnienie między tym, kiedy stosować wyjątki, a kiedy stosować inny mechanizm zgłaszania błędów.
Adrian McCarthy,


1
Nie ma uzasadnienia dla konsensusu. Różni ludzie mają różne opinie na temat „stosowności” rzucania wyjątków, a na te opinie ma generalnie wpływ język, w którym się rozwijają. Oznaczyłeś to pytanie w C ++, ale podejrzewam, że gdybyś oznaczył je Javą, otrzymałeś różne opinie.
Charles Salvia,

5
Nie, to nie powinna być wiki. Odpowiedzi tutaj wymagają specjalistycznej wiedzy i powinny zostać nagrodzone przedstawicielem.
bobobobo

Odpowiedzi:


92

Podstawowym punktem tarcia jest semantyka. Wielu programistów nadużywa wyjątków i rzuca je przy każdej okazji. Chodzi o to, aby użyć wyjątku w nieco wyjątkowej sytuacji. Na przykład błędne dane wejściowe użytkownika nie są traktowane jako wyjątek, ponieważ oczekujesz, że tak się stanie i jesteś na to gotowy. Ale jeśli próbowałeś utworzyć plik i na dysku nie było wystarczającej ilości miejsca, to tak, jest to zdecydowany wyjątek.

Innym problemem jest to, że wyjątki są często wyrzucane i połykane. Programiści używają tej techniki, aby po prostu „wyciszyć” program i pozwolić mu działać tak długo, jak to możliwe, aż do całkowitego upadku. To jest bardzo złe. Jeśli nie przetwarzasz wyjątków, jeśli nie zareagujesz odpowiednio, zwalniając niektóre zasoby, jeśli nie zarejestrujesz wystąpienia wyjątku lub przynajmniej nie powiadomisz użytkownika, oznacza to, że nie używasz wyjątku do tego, co mają na myśli.

Odpowiadając bezpośrednio na Twoje pytanie. Wyjątki powinny być rzadko używane, ponieważ wyjątkowe sytuacje są rzadkie, a wyjątki są kosztowne.

Rzadkie, ponieważ nie spodziewasz się awarii programu przy każdym naciśnięciu przycisku lub przy każdym źle sformułowanym wejściu użytkownika. Powiedzmy, że baza danych może nagle nie być dostępna, może zabraknąć miejsca na dysku, niektóre usługi innej firmy, na których polegasz, są w trybie offline, to wszystko może się zdarzyć, ale dość rzadko byłyby to jasne, wyjątkowe przypadki.

Kosztowne, ponieważ zgłoszenie wyjątku przerwie normalny przepływ programu. Środowisko uruchomieniowe będzie rozwijać stos, dopóki nie znajdzie odpowiedniego programu obsługi wyjątków, który może obsłużyć wyjątek. Zbierze również informacje o wywołaniu na całej drodze, które zostaną przekazane do obiektu wyjątku, który otrzyma program obsługi. To wszystko ma swoje koszty.

Nie oznacza to, że nie może być wyjątku od stosowania wyjątków (uśmiech). Czasami może to uprościć strukturę kodu, jeśli zgłosisz wyjątek zamiast przesyłania dalej kodów powrotnych przez wiele warstw. Z reguły, jeśli spodziewasz się częstego wywoływania jakiejś metody i przez połowę czasu odkryjesz jakąś „wyjątkową” sytuację, to lepiej jest znaleźć inne rozwiązanie. Jeśli jednak spodziewasz się normalnego przebiegu operacji przez większość czasu, podczas gdy ta „wyjątkowa” sytuacja może wystąpić tylko w nielicznych rzadkich okolicznościach, wystarczy zgłosić wyjątek.

@Comments: Wyjątek z pewnością może być użyty w mniej wyjątkowych sytuacjach, jeśli może to uczynić twój kod prostszym i łatwiejszym. Ta opcja jest otwarta, ale powiedziałbym, że w praktyce zdarza się to dość rzadko.

Dlaczego używanie ich do sterowania przepływem jest nierozsądne?

Ponieważ wyjątki zakłócają normalny „przepływ sterowania”. Zgłaszasz wyjątek i normalne wykonywanie programu zostaje przerwane, co może potencjalnie pozostawić obiekty w niespójnym stanie, a niektóre otwarte zasoby nie są zwolnione. Jasne, C # ma instrukcję using, która zapewni, że obiekt zostanie usunięty, nawet jeśli z treści using zostanie zgłoszony wyjątek. Ale na razie odejmijmy się od języka. Załóżmy, że framework nie będzie usuwał obiektów za Ciebie. Robisz to ręcznie. Masz pewien system żądań i zwalniania zasobów i pamięci. Masz umowę dotyczącą całego systemu, która jest odpowiedzialna za zwalnianie obiektów i zasobów w jakich sytuacjach. Masz zasady postępowania z zewnętrznymi bibliotekami. Działa świetnie, jeśli program działa zgodnie z normalnym przebiegiem operacji. Ale nagle w środku egzekucji rzucasz wyjątek. Połowa zasobów pozostaje nieużywana. Połowa nie została jeszcze zażądana. Jeśli operacja miała być teraz transakcyjna, jest zepsuta. Twoje zasady obsługi zasobów nie będą działać, ponieważ te części kodu odpowiedzialne za zwalnianie zasobów po prostu nie będą działać. Jeśli ktoś inny chciałby skorzystać z tych zasobów, może znaleźć je w niespójnym stanie i również ulec awarii, ponieważ nie mógł przewidzieć tej konkretnej sytuacji.

Powiedzmy, że chciałeś, aby metoda M () wywołała metodę N (), aby wykonać jakąś pracę i zorganizować jakiś zasób, a następnie zwrócić go z powrotem do M (), który go użyje, a następnie usunie. W porządku. Teraz coś idzie nie tak w N () i rzuca wyjątek, którego się nie spodziewałeś w M (), więc wyjątek przesuwa się na górę, aż może zostać złapany w jakiejś metodzie C (), która nie będzie miała pojęcia, co się dzieje w głębi w N () oraz czy i jak zwolnić niektóre zasoby.

Rzucając wyjątki, tworzysz sposób na wprowadzenie programu w wiele nowych, nieprzewidywalnych stanów pośrednich, które są trudne do przewidzenia, zrozumienia i radzenia sobie. Jest to trochę podobne do używania GOTO. Bardzo trudno jest zaprojektować program, który może losowo przeskakiwać jego wykonywanie z jednej lokalizacji do drugiej. Trudno będzie go również utrzymywać i debugować. Gdy program stanie się bardziej złożony, po prostu stracisz orientację, co, kiedy i gdzie się dzieje, mniej, aby to naprawić.


5
Jeśli porównasz funkcję, która zgłasza wyjątek, z funkcją, która zwraca kod błędu, rozwijanie stosu byłoby takie samo, chyba że czegoś brakuje.
Catskul

9
@Catskul: niekoniecznie. Funkcja powrotu zwraca sterowanie bezpośrednio do bezpośredniego wywołującego. Zgłoszony wyjątek zwraca kontrolę do pierwszego programu obsługi przechwytywania dla tego typu wyjątku (lub jego klasy bazowej lub „...”) w całości bieżącego stosu wywołań.
jon-hanson

5
Należy również zauważyć, że debuggery często domyślnie przerywają pracę w wyjątkach. Debugger bardzo się zepsuje, jeśli używasz wyjątków dla warunków, które nie są niezwykłe.
Qwertie

5
@jon: Stałoby się tak tylko wtedy, gdyby nie został złapany i musiałby uciec z większej liczby zakresów, aby osiągnąć obsługę błędów. W tym samym przypadku przy użyciu wartości zwracanych (a także konieczności przekazywania ich przez wiele zakresów) wystąpiłaby taka sama ilość rozwijania stosu.
Catskul

3
-1 przepraszam. Nie ma sensu mówić o tym, do czego „przeznaczone są” usługi wyjątków. To tylko mechanizm, który można wykorzystać do obsługi wyjątkowych sytuacji, takich jak brak pamięci itp. - ale to nie znaczy, że nie powinny być używane również do innych celów. Chcę zobaczyć, dlaczego nie powinny być używane do innych celów. Chociaż twoja odpowiedź trochę o tym mówi, jest zmieszana z wieloma słowami o tym, do czego „przeznaczone” są wyjątki.
j_random_hacker

61

Chociaż „rzutowanie wyjątków w wyjątkowych okolicznościach” jest prostą odpowiedzią, w rzeczywistości można zdefiniować, jakie są te okoliczności: kiedy warunki wstępne są spełnione, ale warunki końcowe nie mogą być spełnione . Pozwala to na pisanie bardziej rygorystycznych, ściślejszych i bardziej przydatnych warunków końcowych bez poświęcania obsługi błędów; w przeciwnym razie, bez wyjątków, musisz zmienić warunek końcowy, aby uwzględnić każdy możliwy stan błędu.

  • Warunki wstępne muszą być spełnione przed wywołaniem funkcji.
  • Postcondition co gwarantuje funkcyjne po to powraca .
  • Bezpieczeństwo wyjątków określa, jak wyjątki wpływają na wewnętrzną spójność funkcji lub struktury danych i często zajmują się zachowaniem przekazywanym z zewnątrz (np. Funktor, ktor parametru szablonu itp.).

Konstruktorzy

Niewiele można powiedzieć o każdym konstruktorze dla każdej klasy, który mógłby być napisany w C ++, ale jest kilka rzeczy. Najważniejszym z nich jest to, że skonstruowane obiekty (tj. Dla których konstruktorowi udało się zwrócić) zostaną zniszczone. Nie możesz zmodyfikować tego warunku końcowego, ponieważ język zakłada, że ​​jest on prawdziwy, i automatycznie wywoła destruktory. (Technicznie rzecz biorąc, możesz zaakceptować możliwość nieokreślonego zachowania, w przypadku którego język nie daje żadnych gwarancji , ale jest to prawdopodobnie lepiej opisane gdzie indziej.)

Jedyną alternatywą dla wyrzucenia wyjątku, gdy konstruktor nie może się powieść, jest zmodyfikowanie podstawowej definicji klasy („niezmiennej klasy”), aby zezwolić na prawidłowe stany „null” lub zombie, a tym samym pozwolić konstruktorowi „odnieść sukces” poprzez skonstruowanie zombie .

Przykład zombie

Przykładem tej modyfikacji zombie jest std :: ifstream i zawsze musisz sprawdzić jego stan, zanim będziesz mógł go użyć. Ponieważ na przykład std :: string nie jest, zawsze masz gwarancję, że możesz go użyć natychmiast po skonstruowaniu. Wyobraź sobie, że musiałbyś napisać kod, taki jak ten przykład, i gdybyś zapomniał sprawdzić stan zombie, albo po cichu otrzymałbyś niepoprawne wyniki, albo uszkodziłbyś inne części programu:

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

Nawet nazywanie tej metody jest przykładem tego, jak dobry to musisz zmodyfikować klasy niezmiennik i interfejs do sytuacji ciąg można ani przewidzieć, ani poradzić sobie.

Przykład sprawdzania poprawności danych wejściowych

Spójrzmy na typowy przykład: sprawdzanie poprawności danych wejściowych użytkownika. To, że chcemy zezwolić na błędne dane wejściowe, nie oznacza, że funkcja analizująca musi uwzględnić to w swoim warunku końcowym. Oznacza to jednak, że nasz program obsługi musi sprawdzić, czy parser nie działa.

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}

Niestety, pokazuje to dwa przykłady tego, jak zakres C ++ wymaga złamania RAII / SBRM. Przykład w Pythonie, który nie ma tego problemu i pokazuje coś, co chciałbym mieć C ++ - try-else:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n

Warunki wstępne

Warunki wstępne nie muszą być ściśle sprawdzane - ich naruszenie zawsze wskazuje na błąd logiczny i jest to odpowiedzialność dzwoniącego - ale jeśli je sprawdzisz, zgłoszenie wyjątku jest właściwe. (W niektórych przypadkach bardziej odpowiednie jest zwrócenie śmieci lub zawieszenie programu; chociaż te działania mogą być strasznie nieprawidłowe w innych kontekstach. Jak najlepiej radzić sobie z niezdefiniowanym zachowaniem, to inny temat).

W szczególności skontrastuj gałęzie std :: logic_error i std :: runtime_error hierarchii wyjątków stdlib. Pierwsza z nich jest często używana do naruszania warunków wstępnych, podczas gdy druga jest bardziej odpowiednia do naruszeń warunków wstępnych.


5
Przyzwoita odpowiedź, ale po prostu podajesz regułę, a nie uzasadnienie. Czy w takim razie jest racjonalne: Konwencja i styl?
Catskul

6
+1. W ten sposób zaprojektowano wyjątki do użycia, a używanie ich w ten sposób faktycznie poprawia czytelność i możliwość ponownego wykorzystania kodu (IMHO).
Daniel Pryden,

15
Catskul: pierwsza to definicja wyjątkowych okoliczności, a nie uzasadnienie czy konwencja. Jeśli nie możesz zagwarantować warunków końcowych, nie możesz zwrócić . Bez wyjątków musisz sprawić, by warunek końcowy był bardzo szeroki, aby obejmował wszystkie stany błędów, do punktu, w którym jest on praktycznie bezużyteczny. Wygląda na to, że nie odpowiedziałem na twoje dosłowne pytanie „dlaczego miałyby być rzadkie”, ponieważ nie patrzę na nie w ten sposób. Są narzędziem, które pozwala mi na użyteczne zaostrzenie warunków końcowych, jednocześnie pozwalając na wystąpienie błędów (... w wyjątkowych okolicznościach :).

9
+1. Stan przed / po to NAJLEPSZE wyjaśnienie, jakie kiedykolwiek słyszałem.
KitsuneYMG

6
Wiele osób mówi „ale to sprowadza się tylko do konwencji / semantyki”. Tak to prawda. Ale konwencja i semantyka mają znaczenie, ponieważ mają głębokie implikacje dla złożoności, użyteczności i łatwości utrzymania. W końcu czy decyzja o tym, czy formalnie zdefiniować warunki wstępne i warunki końcowe, nie jest również kwestią konwencji i semantyki? A jednak ich użycie może znacznie ułatwić obsługę i konserwację kodu.
Darryl

40
  1. Kosztowne
    wywołania jądra (lub inne wywołania API systemu) w celu zarządzania interfejsami sygnałowymi jądra (systemowymi)
  2. Trudne do analizy
    Wiele problemów związanych z tymgotostwierdzeniem dotyczy wyjątków. Przeskakują potencjalnie duże ilości kodu, często w wielu procedurach i plikach źródłowych. Nie zawsze jest to widoczne po odczytaniu pośredniego kodu źródłowego. (Jest w Javie.)
  3. Nie zawsze przewidywany przez kod pośredni Kod
    , nad którym następuje przeskok, mógł, ale nie musi, zostać napisany z myślą o możliwości wyjścia wyjątku. Jeśli pierwotnie tak napisano, być może nie zostało to utrzymane z myślą o tym. Pomyśl: wycieki pamięci, wycieki deskryptorów plików, wycieki gniazd, kto wie?
  4. Komplikacje związane z konserwacją
    Trudniej jest utrzymać kod, który przeskakuje wokół przetwarzania wyjątków.

24
+1, ogólnie dobre powody. Należy jednak pamiętać, że koszt rozwijania stosu i wywoływania destruktorów (przynajmniej) jest opłacany przez dowolny równoważny funkcjonalnie mechanizm obsługi błędów, np.
Poprzez

Oznaczę to jako odpowiedź, mimo że zgadzam się z j_random. Odwijanie stosu i usuwanie / alokacja zasobów byłyby takie same w każdym równoważnym funkcjonalnie mechanizmie. Jeśli się zgadzasz, kiedy to zobaczysz, po prostu odetnij je z listy, aby odpowiedź była trochę lepsza.
Catskul

14
Julien: komentarz j_random wskazuje, że nie ma porównywalnych oszczędności: int f() { char* s = malloc(...); if (some_func() == error) { free(s); return error; } ... }musisz zapłacić, aby rozwinąć stack, niezależnie od tego, czy robisz to ręcznie, czy w wyjątkowych sytuacjach. Nie można porównywać za pomocą wyjątków bez obsługi błędów w ogóle .

14
1. Kosztowne: przedwczesna optymalizacja jest źródłem wszelkiego zła. 2. Trudne do analizy: w porównaniu z zagnieżdżonymi warstwami kodu powrotu błędu? Z całym szacunkiem się nie zgadzam. 3. Nie zawsze przewidywane przez kod pośredni: w porównaniu z warstwami zagnieżdżonymi nie zawsze radzi sobie z błędami i tłumaczy je w rozsądny sposób? Nowicjusze zawodzą w obu. 4. Powikłania konserwacyjne: jak? Czy zależności zapośredniczone przez kody błędów są łatwiejsze do utrzymania? ... ale wydaje się, że jest to jeden z tych obszarów, w których twórcy obu szkół mają trudności z zaakceptowaniem argumentów innych. Jak ze wszystkim, projektowanie obsługi błędów to kompromis.
Pontus Gagge,

1
Zgodziłbym się, że jest to złożona sprawa i że trudno jest dokładnie odpowiedzieć ogólnikami. Należy jednak pamiętać, że pierwotne pytanie dotyczyło po prostu powodów, dla których podano poradę dotyczącą zapobiegania wyjątkom, a nie wyjaśnienia ogólnej najlepszej praktyki.
DigitalRoss

22

Zgłaszanie wyjątku jest do pewnego stopnia podobne do instrukcji goto. Zrób to dla kontroli przepływu, a skończysz z niezrozumiałym kodem spaghetti. Co gorsza, w niektórych przypadkach nawet nie wiesz, dokąd dokładnie prowadzi skok (tj. Jeśli nie wychwytujesz wyjątku w danym kontekście). To rażąco narusza zasadę „najmniejszego zaskoczenia”, która zwiększa łatwość konserwacji.


5
Jak można użyć wyjątku w innym celu niż kontrola przepływu? Nawet w wyjątkowej sytuacji nadal stanowią mechanizm kontroli przepływu, co ma konsekwencje dla czytelności kodu (zakładamy tutaj: niezrozumiałe). Przypuszczam, że możesz użyć wyjątków, ale zakazać catch, chociaż w takim przypadku prawie równie dobrze możesz po prostu zadzwonić do std::terminate()siebie. Ogólnie rzecz biorąc, wydaje mi się, że ten argument mówi „nigdy nie używaj wyjątków”, a nie „używaj wyjątków tylko rzadko”.
Steve Jessop

5
instrukcja return w funkcji wyprowadza Cię z funkcji. Wyjątki dotyczą tego, kto wie, ile warstw jest w górę - nie ma prostego sposobu, aby to sprawdzić.

2
Steve: Rozumiem twój punkt widzenia, ale nie o to mi chodziło. Wyjątki są dopuszczalne w nieoczekiwanych sytuacjach. Niektórzy ludzie wykorzystują je jako „wczesne powroty”, może nawet jako rodzaj oświadczenia przełączającego.
Erich Kitzmueller

2
Myślę, że to pytanie zakłada, że ​​„nieoczekiwane” nie jest wystarczającym wyjaśnieniem. W pewnym stopniu zgadzam się. Jeśli jakiś warunek uniemożliwia wykonanie funkcji zgodnie z oczekiwaniami, oznacza to, że albo twój program poprawnie obsługuje tę sytuację, albo nie. Jeśli go obsługuje, to jest „oczekiwany”, a kod musi być zrozumiały. Jeśli nie obsługuje go poprawnie, masz kłopoty. O ile nie pozwolisz, aby wyjątek zakończył działanie programu, pewien poziom twojego kodu musi „oczekiwać” tego. A jednak w praktyce, jak mówię w mojej odpowiedzi, jest to dobra zasada, mimo że teoretycznie jest niezadowalająca.
Steve Jessop

2
Dodatkowo, oczywiście, otoki mogą konwertować funkcję rzucającą na zwracającą błąd lub odwrotnie. Jeśli więc dzwoniący nie zgadza się z Twoim pomysłem na „nieoczekiwane”, niekoniecznie oznacza to, że jego kod jest zrozumiały. Może poświęcić trochę wydajności, gdy prowokują wiele warunków, które uważasz za „nieoczekiwane”, ale myślą, że są normalne i możliwe do odzyskania, a zatem łapią i przekształcają w errno lub cokolwiek innego.
Steve Jessop

16

Wyjątki utrudniają wnioskowanie o stanie programu. Na przykład w C ++ musisz zrobić dodatkowe przemyślenia, aby upewnić się, że Twoje funkcje są silnie bezpieczne pod względem wyjątków, niż musiałbyś to zrobić, gdyby nie musiały.

Powodem jest to, że bez wyjątków wywołanie funkcji może zwrócić albo najpierw zakończyć program. Z wyjątkami, wywołanie funkcji może albo zwrócić, albo zakończyć program, albo przeskoczyć gdzieś do bloku catch. Nie możesz więc już śledzić przepływu kontroli, patrząc tylko na kod przed sobą. Musisz wiedzieć, czy wywoływane funkcje mogą rzucać. Być może będziesz musiał wiedzieć, co można rzucić i gdzie jest złapane, w zależności od tego, czy zależy Ci na tym, gdzie idzie kontrola, czy tylko zależy Ci na tym, aby opuściła obecny zakres.

Z tego powodu ludzie mówią „nie używaj wyjątków, chyba że sytuacja jest naprawdę wyjątkowa”. Kiedy już się do tego zabierasz, „naprawdę wyjątkowa” oznacza „zaistniała sytuacja, w której korzyści z obsługi jej z błędem zwracanej wartości przeważają nad kosztami”. Więc tak, jest to coś w rodzaju pustego stwierdzenia, chociaż gdy już masz instynkt „naprawdę wyjątkowego”, staje się to dobrą zasadą. Kiedy ludzie mówią o kontroli przepływu, mają na myśli, że zdolność rozumowania lokalnego (bez odwoływania się do bloków catch) jest zaletą zwracanych wartości.

Java ma szerszą definicję „naprawdę wyjątkowego” niż C ++. Programiści C ++ z większym prawdopodobieństwem będą chcieli spojrzeć na zwracaną wartość funkcji niż programiści Java, więc w Javie „naprawdę wyjątkowy” może oznaczać „Nie mogę zwrócić obiektu o wartości innej niż NULL jako wyniku tej funkcji”. W C ++ bardziej prawdopodobne jest, że oznacza to „Bardzo wątpię, czy mój rozmówca może kontynuować”. Tak więc strumień Java zgłasza, jeśli nie może odczytać pliku, podczas gdy strumień C ++ (domyślnie) zwraca wartość wskazującą na błąd. Jednak we wszystkich przypadkach jest to kwestia tego, jaki kod chcesz zmusić dzwoniącego do napisania. Tak więc jest to rzeczywiście kwestia stylu kodowania: musisz dojść do porozumienia, jak powinien wyglądać Twój kod i ile kodu „sprawdzającego błędy” chcesz napisać w porównaniu z tym, ile „bezpieczeństwa wyjątków”

We wszystkich językach panuje powszechna zgoda co do tego, że najlepiej jest to zrobić pod względem tego, jak prawdopodobny jest możliwy do naprawienia błąd (ponieważ nieodwracalne błędy skutkują brakiem kodu z wyjątkami, ale nadal wymagają sprawdzenia i zwrócenia własnego błąd w kodzie, który używa zwracanych błędów). Ludzie oczekują więc, że „ta funkcja, którą wywołuję, zgłasza wyjątek”, co oznacza „ nie mogę kontynuować”, a nie tylko „ tonie można kontynuować ”. Nie jest to nieodłączne od wyjątków, to tylko zwyczaj, ale jak każda dobra praktyka programistyczna, jest to zwyczaj zalecany przez inteligentnych ludzi, którzy próbowali tego w inny sposób i nie cieszyli się z rezultatów. Ja też miałem złe doświadczenia rzucające zbyt wiele wyjątków, więc osobiście myślę w kategoriach „naprawdę wyjątkowe”, chyba że coś w tej sytuacji czyni wyjątek szczególnie atrakcyjnym.

Przy okazji, poza rozumowaniem dotyczącym stanu twojego kodu, istnieją również konsekwencje dla wydajności. Wyjątki są teraz zazwyczaj tanie, w językach, w których masz prawo dbać o wydajność. Mogą być szybsze niż wiele poziomów „och, wynik jest błędem, w takim razie też bym wyszedł z błędem”. W dawnych złych czasach istniały prawdziwe obawy, że rzucenie wyjątku, złapanie go i kontynuowanie następnej rzeczy spowoduje, że to, co robisz, będzie tak wolne, że stanie się bezużyteczne. Zatem w tym przypadku „naprawdę wyjątkowa” oznacza „sytuacja jest tak zła, że ​​przerażające wykonanie nie ma już znaczenia”. Tak już nie jest (chociaż wyjątek w wąskiej pętli jest nadal zauważalny) i miejmy nadzieję, że wskazuje, dlaczego definicja „naprawdę wyjątkowego” musi być elastyczna.


2
„Na przykład w C ++ musisz przemyśleć więcej, aby upewnić się, że Twoje funkcje są silnie bezpieczne dla wyjątków, niż byłoby to konieczne, gdyby nie było takiej potrzeby”. - biorąc pod uwagę, że podstawowe konstrukcje C ++ (takie jak new) i biblioteka standardowa wszystkie zgłaszają wyjątki, nie widzę, w jaki sposób nieużywanie wyjątków w kodzie zwalnia Cię z pisania kodu bezpiecznego dla wyjątków niezależnie.
Pavel Minaev

1
Musiałbyś zapytać Google. Ale tak, uwaga, jeśli twoja funkcja nie jest nothrow, wtedy wywoływacze będą musieli trochę popracować, a dodanie dodatkowych warunków, które powodują wyjątki, nie powoduje, że jest to „bardziej rzucanie wyjątków”. Ale brak pamięci to i tak trochę szczególny przypadek. Często zdarza się, że programy nie obsługują tego i po prostu zamykają się. Mógłbyś mieć standard kodowania, który mówi: „nie używaj wyjątków, nie dawaj gwarancji wyjątków, jeśli nowe rzuty, wtedy zakończymy działanie i użyjemy kompilatorów, które nie rozwijają stosu”. Niezbyt ambitny, ale poleci.
Steve Jessop

Gdy użyjesz kompilatora, który nie rozwija stosu (lub w inny sposób nie wyłącza w nim wyjątków), technicznie rzecz biorąc, nie jest to już C ++ :)
Pavel Minaev

Chodzi mi o to, że nie rozwija stosu w przypadku nieprzechwyconego wyjątku.
Steve Jessop

Skąd wie, że nie jest złapany, jeśli nie rozwija stosu, aby znaleźć blok catch?
jmucchiello

11

Naprawdę nie ma konsensusu. Cała kwestia jest nieco subiektywna, ponieważ „stosowność” rzucenia wyjątku jest często sugerowana przez istniejące praktyki w standardowej bibliotece samego języka. Biblioteka standardowa C ++ rzuca wyjątki znacznie rzadziej niż powiedzmy, standardowa biblioteka Java, która prawie zawsze preferuje wyjątki, nawet w przypadku oczekiwanych błędów, takich jak nieprawidłowe dane wejściowe użytkownika (np Scanner.nextInt.). Uważam, że ma to znaczący wpływ na opinie programistów o tym, kiedy należy zgłosić wyjątek.

Jako programista C ++ osobiście wolę zarezerwować wyjątki dla bardzo "wyjątkowych" okoliczności, np. Brak pamięci, brak miejsca na dysku, nastąpiła apokalipsa itp. Ale nie nalegam, że jest to absolutnie poprawny sposób rzeczy.


2
Myślę, że istnieje pewnego rodzaju konsensus, ale być może jest on oparty bardziej na konwencji niż na zdrowym rozumowaniu. Mogą istnieć rozsądne powody, aby używać wyjątków tylko w wyjątkowych okolicznościach, ale większość programistów nie jest ich tak naprawdę świadoma.
Qwertie

4
Zgoda - często słyszysz o „wyjątkach dotyczą wyjątkowych okoliczności”, ale nikt nie zadaje sobie trudu, aby właściwie zdefiniować, czym jest „wyjątkowa sytuacja” - jest to w dużej mierze zwyczajowe i zdecydowanie specyficzne dla języka. Heck, w Pythonie iteratory używają wyjątku do sygnalizowania końca sekwencji i jest to uważane za całkowicie normalne!
Pavel Minaev

2
+1. Nie ma sztywnych i szybkich reguł, tylko konwencje - ale warto przestrzegać konwencji innych, którzy używają Twojego języka, ponieważ ułatwia to programistom wzajemne zrozumienie kodu.
j_random_hacker

1
„wydarzyła się apokalipsa” - nieważne wyjątki, jeśli cokolwiek usprawiedliwia UB, to tak ;-)
Steve Jessop

3
Catskul: Prawie całe programowanie jest konwencją. Technicznie rzecz biorąc, nie potrzebujemy nawet wyjątków, lub naprawdę wcale. Jeśli nie dotyczy to NP-zupełności, big-O / theta / little-o lub Universal Turing Machines, to prawdopodobnie jest to konwencja. :-)
Ken

7

Nie sądzę, aby wyjątki były rzadko używane. Ale.

Nie wszystkie zespoły i projekty są gotowe do korzystania z wyjątków. Korzystanie z wyjątków wymaga wysokich kwalifikacji programistów, specjalnych technik i braku dużego, starszego kodu niezabezpieczającego wyjątków. Jeśli masz ogromną starą bazę kodów, prawie zawsze nie jest ona bezpieczna. Jestem pewien, że nie chcesz tego przepisać.

Jeśli zamierzasz intensywnie korzystać z wyjątków, to:

  • bądź przygotowany, aby uczyć swoich ludzi, czym jest bezpieczeństwo w wyjątkowych sytuacjach
  • nie powinieneś używać surowego zarządzania pamięcią
  • używać RAII w szerokim zakresie

Z drugiej strony, używanie wyjątków w nowych projektach z silnym zespołem może sprawić, że kod będzie czystszy, łatwiejszy w utrzymaniu, a nawet szybszy:

  • nie przegapisz ani nie zignorujesz błędów
  • nie musisz pisać tego sprawdzania kodów powrotu, nie wiedząc, co zrobić z niewłaściwym kodem na niskim poziomie
  • kiedy jesteś zmuszony pisać kod bezpieczny dla wyjątków, staje się on bardziej uporządkowany

1
Szczególnie podobała mi się twoja wzmianka o tym, że „nie wszystkie zespoły… są gotowe do korzystania z wyjątków”. Wyjątki są zdecydowanie czymś, co wydaje się łatwe do zrobienia, ale jest niezwykle trudne do zrobienia dobrze , i to po części czyni je niebezpiecznymi.
j_random_hacker

1
+1 dla „gdy jesteś zmuszony pisać kod bezpieczny dla wyjątków, staje się on bardziej uporządkowany”. Kod oparty na wyjątkach musi mieć większą strukturę i uważam, że o wiele łatwiej jest wnioskować o obiektach i niezmiennikach, gdy nie można ich zignorować. W rzeczywistości uważam, że silne bezpieczeństwo wyjątków polega na pisaniu prawie odwracalnego kodu, co bardzo ułatwia uniknięcie stanu nieokreślonego.
Tom

7

EDYCJA 20.11.2009 :

Właśnie czytałem ten artykuł MSDN dotyczący poprawy wydajności kodu zarządzanego i ta część przypomniała mi o tym pytaniu:

Koszt wydajności rzucania wyjątku jest znaczny. Chociaż obsługa wyjątków strukturalnych jest zalecanym sposobem obsługi warunków błędów, upewnij się, że wyjątki są używane tylko w wyjątkowych okolicznościach, gdy wystąpią warunki błędu. Nie używaj wyjątków dla zwykłego przepływu sterowania.

Oczywiście dotyczy to tylko platformy .NET i jest również skierowane do osób tworzących aplikacje o wysokiej wydajności (takich jak ja); więc oczywiście nie jest to uniwersalna prawda. Mimo to jest nas wielu programistów .NET, więc uznałem, że warto to zauważyć.

EDYCJA :

OK, po pierwsze, wyjaśnijmy jedną rzecz: nie mam zamiaru z nikim walczyć o kwestię wydajności. Ogólnie rzecz biorąc, jestem skłonny zgodzić się z tymi, którzy uważają, że przedwczesna optymalizacja jest grzechem. Pozwólcie jednak, że przedstawię dwie kwestie:

  1. Plakat prosi o obiektywne uzasadnienie konwencjonalnej opinii, że wyjątki powinny być używane oszczędnie. Możemy rozmawiać o czytelności i odpowiednim projekcie, ile tylko chcemy; ale są to sprawy subiektywne, z ludźmi gotowymi do kłótni po obu stronach. Myślę, że plakat jest tego świadomy. Faktem jest, że używanie wyjątków do sterowania przepływem programu jest często nieefektywnym sposobem wykonywania pewnych czynności. Nie, nie zawsze , ale często . Dlatego rozsądną radą jest oszczędne stosowanie wyjątków, podobnie jak oszczędne jedzenie czerwonego mięsa lub picie wina.

  2. Istnieje różnica między optymalizacją bez powodu a pisaniem wydajnego kodu. Konsekwencją tego jest to, że istnieje różnica między pisaniem czegoś, co jest solidne, jeśli nie zoptymalizowane, a czymś, co jest po prostu nieefektywne. Czasami myślę, że kiedy ludzie kłócą się o takie rzeczy, jak obsługa wyjątków, tak naprawdę rozmawiają między sobą, ponieważ omawiają zasadniczo różne rzeczy.

Aby zilustrować mój punkt widzenia, rozważ następujące przykłady kodu w języku C #.

Przykład 1: Wykrywanie nieprawidłowych danych wejściowych użytkownika

To jest przykład tego, co nazwałbym nadużyciem wyjątków .

int value = -1;
string input = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        value = int.Parse(input);
        inputChecksOut = true;

    } catch (FormatException) {
        input = GetInput();
    }
}

Ten kod jest dla mnie śmieszny. Oczywiście, że działa . Nikt się z tym nie spiera. Ale powinno to być coś takiego:

int value = -1;
string input = GetInput();

while (!int.TryParse(input, out value)) {
    input = GetInput();
}

Przykład 2: Sprawdzanie istnienia pliku

Myślę, że ten scenariusz jest w rzeczywistości bardzo powszechny. Z pewnością wielu ludziom wydaje się to o wiele bardziej „akceptowalne”, ponieważ dotyczy operacji we / wy plików:

string text = null;
string path = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        using (FileStream fs = new FileStream(path, FileMode.Open)) {
            using (StreamReader sr = new StreamReader(fs)) {
                text = sr.ReadToEnd();
            }
        }

        inputChecksOut = true;

    } catch (FileNotFoundException) {
        path = GetInput();
    }
}

Wydaje się to rozsądne, prawda? Próbujemy otworzyć plik; jeśli go tam nie ma, wychwytujemy ten wyjątek i próbujemy otworzyć inny plik ... Co w tym złego?

Nic takiego. Ale rozważ tę alternatywę, która nie rzuca żadnych wyjątków:

string text = null;
string path = GetInput();

while (!File.Exists(path)) path = GetInput();

using (FileStream fs = new FileStream(path, FileMode.Open)) {
    using (StreamReader sr = new StreamReader(fs)) {
        text = sr.ReadToEnd();
    }
}

Oczywiście, gdyby wyniki tych dwóch podejść były faktycznie takie same, byłaby to naprawdę kwestia czysto doktrynalna. Więc spójrzmy. Dla pierwszego przykładu kodu utworzyłem listę 10000 losowych ciągów, z których żaden nie reprezentował prawidłowej liczby całkowitej, a następnie dodałem prawidłowy ciąg liczb całkowitych na samym końcu. Stosując oba powyższe podejścia, moje wyniki były następujące:

Korzystanie try/ catchblok: 25,455 sekund
Korzystanie int.TryParse: 1,637 milisekund

W drugim przykładzie zrobiłem w zasadzie to samo: utworzyłem listę 10000 losowych ciągów, z których żaden nie był prawidłową ścieżką, a następnie dodałem prawidłową ścieżkę na samym końcu. Oto wyniki:

Korzystanie try/ catchblok: 29,989 sekund
Korzystanie File.Exists: 22,820 milisekund

Wiele osób zareagowałoby na to, mówiąc: „No cóż, rzucanie i łapanie 10000 wyjątków jest skrajnie nierealne; to przesadza z wynikami”. Oczywiście, że tak. Różnica między wyrzuceniem jednego wyjątku a samodzielną obsługą złych danych wejściowych nie będzie zauważalna dla użytkownika. Faktem jest, że używanie wyjątków jest w tych dwóch przypadkach od 1000 do ponad 10 000 razy wolniejsze niż podejścia alternatywne, które są równie czytelne - jeśli nie bardziej.

Dlatego GetNine()poniżej podałem przykład metody. Nie chodzi o to, że jest nieznośnie powolny lub niedopuszczalnie wolny; chodzi o to, że jest wolniejszy niż powinien ... bez powodu .

Ponownie, to tylko dwa przykłady. Od kursu nie będzie wtedy, gdy hit wydajność wykorzystania wyjątków nie jest to ciężkie (Pavel ma rację, po wszystkim, to jest uzależnione od wykonania) jest. Mówię tylko: spójrzmy prawdzie w oczy, chłopaki - w przypadkach takich jak ten powyżej, rzucanie i łapanie wyjątku jest analogiczne do GetNine(); to po prostu nieefektywny sposób na zrobienie czegoś , co można z łatwością zrobić lepiej .


Prosisz o uzasadnienie, jakby to była jedna z tych sytuacji, w których wszyscy wskoczyli na modę, nie wiedząc dlaczego. Ale w rzeczywistości odpowiedź jest oczywista i myślę, że już ją znasz. Obsługa wyjątków ma horrendalną wydajność.

OK, może to dobrze dla twojego scenariusza biznesowego, ale relatywnie rzecz biorąc , rzucanie / wychwytywanie wyjątku wprowadza znacznie więcej narzutów niż jest to konieczne w wielu, wielu przypadkach. Wiesz to, ja to wiem: przez większość czasu , jeśli używasz wyjątków do sterowania przepływem programu, po prostu piszesz wolny kod.

Równie dobrze możesz zapytać: dlaczego ten kod jest zły?

private int GetNine() {
    for (int i = 0; i < 10; i++) {
        if (i == 9) return i;
    }
}

Założę się, że jeśli sprofilujesz tę funkcję, okaże się, że działa ona dość szybko w przypadku typowej aplikacji biznesowej. Nie zmienia to faktu, że jest to okropnie nieefektywny sposób na osiągnięcie czegoś, co można zrobić o wiele lepiej.

To właśnie mają na myśli ludzie, gdy mówią o wyjątkowych „nadużyciach”.


2
„Obsługa wyjątków ma horrendalną wydajność”. - to szczegół implementacji, który nie jest prawdziwy dla wszystkich języków, a nawet dla C ++ w szczególności dla wszystkich implementacji.
Pavel Minaev

„to szczegół implementacji” Nie pamiętam, jak często słyszałem ten argument - i po prostu śmierdzi. Pamiętaj: wszystkie rzeczy związane z komputerem dotyczą sumy szczegółów implementacji. A także: nie jest mądrze używać śrubokręta zamiast młotka - to właśnie robią ludzie, którzy mówią dużo o „tylko szczegółach implementacji”.
Juergen

2
A jednak Python szczęśliwie używa wyjątków do ogólnej kontroli przepływu, ponieważ trafienie perf w ich implementacji nie jest tak złe. Więc jeśli twój młotek wygląda jak śrubokręt, cóż, obwiniaj młotek ...
Pavel Minaev

1
@j_random_hacker: Masz rację, GetNinewykonuje swoją pracę dobrze. Chodzi mi o to, że nie ma powodu, aby go używać. To nie jest tak, że jest to „szybki i łatwy” sposób na zrobienie czegoś, podczas gdy bardziej „poprawne” podejście wymagałoby znacznie więcej wysiłku. Z mojego doświadczenia wynika, że ​​często „prawidłowe” podejście wymagałoby mniej wysiłku lub mniej więcej tyle samo. W każdym razie wydajność jest dla mnie jedynym obiektywnym powodem, dla którego odradza się używanie wyjątków po lewej i prawej stronie. Jeśli chodzi o to, czy utrata wydajności jest „rażąco przeszacowana”, jest to w rzeczywistości subiektywne.
Dan Tao

1
@j_random_hacker: Twoja strona jest bardzo interesująca i naprawdę kieruje uwagę Pawła na temat wydajności w zależności od implementacji. Jedną rzeczą, w którą bardzo wątpię, jest to, że ktoś mógłby znaleźć scenariusz, w którym stosowanie wyjątków faktycznie przewyższa alternatywę (jeśli istnieje alternatywa, przynajmniej tak jak w moich przykładach).
Dan Tao

6

Wszystkie praktyczne zasady dotyczące wyjątków sprowadzają się do subiektywnych warunków. Nie powinieneś spodziewać się twardych i szybkich definicji, kiedy ich używać, a kiedy nie. „Tylko w wyjątkowych okolicznościach”. Ładna okólnikowa definicja: wyjątki dotyczą wyjątkowych okoliczności.

Kiedy używać wyjątków, należy do tego samego zasobnika, co „skąd mam wiedzieć, czy ten kod jest jedną klasą czy dwiema?” To po części kwestia stylistyczna, po części preferencja. Wyjątki to narzędzie. Mogą być używane i nadużywane, a znalezienie granicy między nimi jest częścią sztuki i umiejętności programowania.

Istnieje wiele opinii i kompromisów. Znajdź coś, co do ciebie przemawia i podążaj za tym.


6

Nie chodzi o to, że wyjątki powinny być rzadko używane. Po prostu powinny być rzucane tylko w wyjątkowych okolicznościach. Na przykład, jeśli użytkownik wprowadzi nieprawidłowe hasło, nie jest to wyjątkowe.

Powód jest prosty: wyjątki nagle kończą funkcję i propagują stos do catchbloku. Ten proces jest bardzo kosztowny obliczeniowo: C ++ buduje swój system wyjątków tak, aby mieć niewielki narzut na "normalne" wywołania funkcji, więc kiedy wyjątek jest zgłaszany, musi wykonać dużo pracy, aby znaleźć miejsce, do którego należy się udać. Co więcej, ponieważ każda linia kodu mogłaby zgłosić wyjątek. Jeśli mamy jakąś funkcję, fktóra często wywołuje wyjątki, musimy teraz uważać, aby używać naszego try/ catchbloki wokół każdego wywołania f. To dość złe połączenie interfejsu / implementacji.


8
Zasadniczo powtórzyłeś uzasadnienie, że „nie bez powodu nazywają się wyjątkami”. Szukam ludzi, którzy przekształcą to w coś bardziej treściwego.
Catskul

4
Co oznaczają „wyjątkowe okoliczności”? W praktyce mój program musi częściej obsługiwać rzeczywiste IOExceptions niż złe hasła użytkowników. Czy to oznacza, że ​​myślisz, że powinienem uczynić BadUserPassword wyjątkiem, czy też faceci z biblioteki standardowej nie powinni byli uczynić IOException wyjątku? „Bardzo kosztowne obliczeniowo” nie może być faktycznym powodem, ponieważ nigdy nie widziałem programu (a już na pewno nie mojego), dla którego obsługa nieprawidłowego hasła za pośrednictwem dowolnego mechanizmu kontrolnego byłaby wąskim gardłem wydajności.
Ken

5

Wspomniałem o tym w artykule o wyjątkach C ++ .

Odpowiednia część:

Niemal zawsze używanie wyjątków do wpływania na „normalny” przepływ jest złym pomysłem. Jak już omówiliśmy w sekcji 3.1, wyjątki generują niewidoczne ścieżki kodu. Te ścieżki kodu są prawdopodobnie dopuszczalne, jeśli są wykonywane tylko w scenariuszach obsługi błędów. Jeśli jednak użyjemy wyjątków w jakimkolwiek innym celu, nasze „normalne” wykonanie kodu jest podzielone na widoczną i niewidoczną część, co sprawia, że ​​kod jest bardzo trudny do odczytania, zrozumienia i rozszerzenia.


1
Widzisz, przyszedłem do C ++ z C i myślę, że „scenariusze obsługi błędów” są normalne. Mogą nie być wykonywane często, ale spędzam więcej czasu na myśleniu o nich niż o ścieżkach kodu bez błędów. Więc znowu, ogólnie zgadzam się z tym sentymentem, ale nie przybliża nas to ani do zdefiniowania „normalnego”, ani do tego, jak możemy wywnioskować z definicji „normalnego”, że używanie wyjątków jest złą opcją.
Steve Jessop

Przeszedłem również do C ++ z C, ale nawet w CI rozróżniłem „normalną” obsługę błędów końca przepływu. Jeśli wywołasz np. Fopen (), istnieje gałąź, która zajmuje się przypadkiem sukcesu i jest „normalna”, oraz taka, która zajmuje się możliwymi przyczynami niepowodzenia, a jest nią obsługa błędów.
Nemanja Trifunovic

2
Zgadza się, ale w tajemniczy sposób nie rozumiem kodu błędu. Więc jeśli „niewidoczne ścieżki kodu” są bardzo trudne do odczytania, zrozumienia i rozszerzenia dla „normalnego” kodu, to w ogóle nie rozumiem, dlaczego użycie słowa „błąd” czyni je „prawdopodobnie akceptowalnymi”. Z mojego doświadczenia wynika, że ​​sytuacje błędów zdarzają się cały czas, co czyni je „normalnymi”. Jeśli potrafisz wykryć wyjątki w sytuacjach błędów, możesz je znaleźć w sytuacjach bez błędów. Jeśli nie możesz, nie możesz, a wszystko, co zrobiłeś, to uczynienie kodu błędu nieprzeniknionym, ale zignoruj ​​ten fakt, ponieważ są to „tylko błędy”.
Steve Jessop

1
Przykład: masz funkcję, w której coś trzeba zrobić, ale zamierzasz wypróbować całą masę różnych podejść i nie wiesz, które się powiedzie, a każde z nich może wypróbować dodatkowe pod-podejścia, i tak dalej, aż coś zadziała. Wtedy argumentowałbym, że porażka jest „normalna”, a sukces jest „wyjątkowy”, chociaż wyraźnie nie jest błędem. Rzucanie wyjątku w przypadku sukcesu nie jest trudniejsze do zrozumienia niż rzucanie wyjątku w przypadku okropnego błędu w większości programów - upewniasz się, że tylko jeden fragment kodu musi wiedzieć, co robić dalej, i skaczesz od razu do tego miejsca.
Steve Jessop

1
@onebyone: Twój drugi komentarz właściwie mówi o słuszności, której nie widziałem gdzie indziej. Myślę, że odpowiedź na to pytanie jest taka, że ​​(jak skutecznie powiedziałeś w swojej własnej odpowiedzi) używanie wyjątków dla warunków błędu zostało wchłonięte przez „standardowe praktyki” wielu języków, co czyni je użyteczną wskazówką, której należy się trzymać, nawet jeśli pierwotne powody czynienie tego było błędne.
j_random_hacker

5

Moje podejście do obsługi błędów jest takie, że istnieją trzy podstawowe typy błędów:

  • Dziwna sytuacja, którą można obsłużyć w witrynie błędu. Może tak być, jeśli użytkownik wprowadzi niepoprawne dane wejściowe w wierszu poleceń. Prawidłowe zachowanie polega po prostu na złożeniu skargi do użytkownika i zapętleniu w tym przypadku. Inną sytuacją może być dzielenie przez zero. Te sytuacje nie są tak naprawdę sytuacjami błędnymi i są zwykle spowodowane błędnymi danymi wejściowymi.
  • Sytuacja podobna do poprzedniej, ale taka, której nie można rozwiązać w witrynie błędu. Na przykład, jeśli masz funkcję, która pobiera nazwę pliku i analizuje plik o tej nazwie, może nie być w stanie otworzyć pliku. W takim przypadku nie radzi sobie z błędem. To właśnie wtedy lśnią wyjątki. Zamiast używać podejścia C (zwrócić nieprawidłową wartość jako flagę i ustawić globalną zmienną błędu, aby wskazać problem), kod może zamiast tego zgłosić wyjątek. Kod wywołujący będzie wtedy mógł obsłużyć wyjątek - na przykład poprosić użytkownika o inną nazwę pliku.
  • Sytuacja, która nie powinna mieć miejsca. Dzieje się tak, gdy naruszona jest niezmiennicza klasa lub funkcja otrzymuje nieprawidłowy parametr lub tym podobne. Wskazuje to na błąd logiczny w kodzie. W zależności od stopnia niepowodzenia wyjątek może być odpowiedni lub preferowane może być wymuszenie natychmiastowego zakończenia (podobnie assertjak). Ogólnie rzecz biorąc, te sytuacje wskazują, że coś się zepsuło gdzieś w kodzie i nie można w praktyce ufać, że cokolwiek innego jest poprawne - może dojść do gwałtownego uszkodzenia pamięci. Twój statek tonie, wysiadaj.

Parafrazując, wyjątki dotyczą sytuacji, gdy masz problem, z którym możesz sobie poradzić, ale nie możesz sobie z nim poradzić w miejscu, w którym go zauważysz. Problemy, z którymi nie możesz sobie poradzić, powinny po prostu zabić program; problemy, z którymi możesz sobie poradzić od razu, powinny być po prostu rozwiązane.


2
Odpowiadasz na złe pytanie. Nie chcemy wiedzieć, dlaczego powinniśmy (lub nie powinniśmy) rozważyć użycie wyjątków do obsługi scenariuszy błędów - chcemy wiedzieć, dlaczego powinniśmy (lub nie powinniśmy) ich używać w scenariuszach bez obsługi błędów.
j_random_hacker

5

Tutaj przeczytałem niektóre odpowiedzi. Nadal jestem zdumiony tym, o co chodzi w tym zamieszaniu. Zdecydowanie nie zgadzam się z tymi wszystkimi wyjątkami == spagetty kod. Przez zamieszanie mam na myśli, że są ludzie, którzy nie doceniają obsługi wyjątków w C ++. Nie jestem pewien, jak dowiedziałem się o obsłudze wyjątków w C ++ - ale zrozumiałem konsekwencje w ciągu kilku minut. Było to około 1996 roku i korzystałem z kompilatora borland C ++ dla OS / 2. Nigdy nie miałem problemu z podjęciem decyzji, kiedy używać wyjątków. Zwykle zawijam omylne akcje do-cofnięcia w klasy C ++. Takie czynności do-cofnięcia obejmują:

  • tworzenie / niszczenie uchwytu systemowego (dla plików, map pamięci, uchwytów GUI WIN32, gniazd itp.)
  • uzbrojenie / rozbrojenie obsługi
  • alokowanie / zwalnianie pamięci
  • zgłoszenie roszczenia / zwolnienie muteksu
  • zwiększanie / zmniejszanie liczby referencyjnej
  • pokazywanie / ukrywanie okna

Niż są funkcjonalne opakowania. Aby zawinąć wywołania systemowe (które nie należą do poprzedniej kategorii) w C ++. Np. Odczyt / zapis z / do pliku. Jeśli coś się nie powiedzie, zostanie zgłoszony wyjątek, który zawiera pełne informacje o błędzie.

Następnie następuje przechwytywanie / ponowne zgłaszanie wyjątków, aby dodać więcej informacji o niepowodzeniu.

Ogólna obsługa wyjątków C ++ prowadzi do bardziej przejrzystego kodu. Ilość kodu jest drastycznie zmniejszona. Wreszcie można użyć konstruktora do alokacji omylnych zasobów i nadal utrzymywać środowisko wolne od korupcji po takiej awarii.

Takie klasy można łączyć w klasy złożone. Po wykonaniu konstruktora jakiegoś obiektu składowego / podstawowego można polegać na tym, że wszystkie inne konstruktory tego samego obiektu (wykonane wcześniej) zostały pomyślnie wykonane.


3

Wyjątki to bardzo nietypowa metoda sterowania przepływem w porównaniu z tradycyjnymi konstrukcjami (pętle, if, funkcje itp.). Normalne konstrukcje przepływu sterowania (pętle, if, wywołania funkcji itp.) Mogą obsłużyć wszystkie normalne sytuacje. Jeśli okaże się, że sięgasz po wyjątek dla rutynowego wystąpienia, być może musisz rozważyć strukturę swojego kodu.

Ale są pewne typy błędów, których nie da się łatwo obsłużyć za pomocą normalnych konstrukcji. Katastrofalne awarie (takie jak awaria alokacji zasobów) można wykryć na niskim poziomie, ale prawdopodobnie nie można ich tam obsłużyć, więc proste stwierdzenie „jeśli” jest niewystarczające. Tego typu awarie zazwyczaj wymagają obsługi na znacznie wyższym poziomie (np. Zapisz plik, zarejestruj błąd, zakończ). Próba zgłoszenia takiego błędu za pomocą tradycyjnych metod (takich jak zwracane wartości) jest żmudna i podatna na błędy. Ponadto wstrzykuje obciążenie do warstw interfejsów API średniego poziomu, aby poradzić sobie z tą dziwną, niezwykłą awarią. Narzuty rozpraszają uwagę klientów tych interfejsów API i wymagają od nich martwienia się o problemy, na które nie mają wpływu. Wyjątki umożliwiają nielokalną obsługę dużych błędów, które „

Jeśli klient wywołuje ParseIntciąg, a łańcuch nie zawiera liczby całkowitej, wówczas bezpośredni wywołujący prawdopodobnie przejmuje się błędem i wie, co z nim zrobić. Więc zaprojektowałbyś ParseInt tak, aby zwracał kod błędu dla czegoś takiego.

Z drugiej strony, jeśli ParseIntzawiedzie, ponieważ nie może przydzielić bufora, ponieważ pamięć jest strasznie pofragmentowana, wówczas dzwoniący nie będzie wiedział, co z tym zrobić. Musiałby wydobyć ten niezwykły błąd w górę i do jakiejś warstwy, która zajmuje się tymi podstawowymi awariami. To opodatkowuje wszystkich pośrednich (ponieważ muszą dostosować mechanizm przekazywania błędów we własnych interfejsach API). Wyjątek umożliwia pominięcie tych warstw (przy jednoczesnym zapewnieniu niezbędnego czyszczenia).

Podczas pisania kodu niskiego poziomu może być trudno zdecydować, kiedy używać tradycyjnych metod, a kiedy zgłaszać wyjątki. Kod niskiego poziomu musi podjąć decyzję (rzut lub nie). Ale to kod wyższego poziomu naprawdę wie, czego się oczekuje i co jest wyjątkowe.


1
A jednak, w Javie, parseIntfaktycznie nie wyjątek. Zatem powiedziałbym, że opinie o stosowności rzucania wyjątków są w dużym stopniu zależne od wcześniej istniejących praktyk w standardowych bibliotekach wybranego języka programowania.
Charles Salvia

Środowisko wykonawcze Javy i tak musi śledzić informacje, co ułatwia generowanie wyjątków ... Kod c ++ zwykle nie ma tych informacji na rękach, więc generowanie ich wiąże się z obniżeniem wydajności. Nie trzymając go pod ręką, wydaje się być szybszy / mniejszy / bardziej przyjazny dla pamięci podręcznej itp. Ponadto kod java ma dynamiczne sprawdzanie rzeczy, takich jak tablice itp., Funkcja nie jest nieodłącznym elementem c ++, więc dodatkowa klasa wyjątków java, którą programista jest już zajmujesz się wieloma z tych rzeczy za pomocą bloków try / catch, więc dlaczego nie użyć wyjątków do WSZYSTKIEGO?
Ape-inago

1
@Charles - Pytanie jest oznaczone tagiem C ++, więc odpowiadałem z tej perspektywy. Nigdy nie słyszałem, żeby programista Java (lub C #) mówił, że wyjątki dotyczą tylko wyjątkowych warunków. Programiści C ++ powtarzają to regularnie, a pytanie brzmiało dlaczego.
Adrian McCarthy

1
W rzeczywistości wyjątki w C # również to mówią, a powodem jest to, że wyjątki są bardzo kosztowne w .NET (bardziej niż np. W Javie).
Pavel Minaev

1
@Adrian: prawda, pytanie jest oznaczone tagiem C ++, chociaż filozofia obsługi wyjątków jest w pewnym sensie dialogiem międzyjęzykowym, ponieważ języki z pewnością wpływają na siebie nawzajem. W każdym razie Boost.lexical_cast zgłasza wyjątek. Ale czy nieprawidłowy ciąg naprawdę jest tak „wyjątkowy”? Prawdopodobnie tak nie jest, ale programiści Boost uznali, że warto mieć taką fajną składnię rzutowania, aby skorzystać z wyjątków. To tylko pokazuje, jak subiektywne jest to wszystko.
Charles Salvia,

3

W C ++ jest kilka powodów.

Po pierwsze, często trudno jest zobaczyć, skąd pochodzą wyjątki (ponieważ można je wyrzucić z prawie wszystkiego), więc blok catch jest czymś w rodzaju instrukcji COME FROM. To jest gorsze niż GO TO, ponieważ w GO TO wiesz, skąd pochodzisz (instrukcja, a nie jakieś przypadkowe wywołanie funkcji) i dokąd zmierzasz (etykieta). Są w zasadzie potencjalnie bezpieczną dla zasobów wersją setjmp () i longjmp () języka C i nikt nie chce ich używać.

Po drugie, C ++ nie ma wbudowanego czyszczenia pamięci, więc klasy C ++, które są właścicielami zasobów, pozbywają się ich w ich destruktorach. Dlatego w obsłudze wyjątków C ++ system musi uruchamiać wszystkie destruktory w zasięgu. W językach z GC i bez prawdziwych konstruktorów, takich jak Java, rzucanie wyjątków jest znacznie mniej uciążliwe.

Po trzecie, społeczność C ++, w tym Bjarne Stroustrup i Komitet Standardów oraz różni autorzy kompilatorów, zakładali, że wyjątki powinny być wyjątkowe. Ogólnie rzecz biorąc, nie warto sprzeciwiać się kulturze językowej. Implementacje opierają się na założeniu, że wyjątki będą rzadkie. Lepsze książki traktują wyjątki jako wyjątkowe. Dobry kod źródłowy wykorzystuje kilka wyjątków. Dobrzy programiści C ++ traktują wyjątki jako wyjątkowe. Aby temu przeciwdziałać, potrzebujesz dobrego powodu, a wszystkie powody, które widzę, są po stronie, aby zachować ich wyjątkowość.


1
„C ++ nie ma wbudowanego usuwania elementów bezużytecznych, więc klasy C ++, które są właścicielami zasobów, pozbywają się ich w swoich destruktorach. Dlatego w C ++ obsłudze wyjątków system musi uruchamiać wszystkie destruktory w zakresie. W językach z GC i bez rzeczywistych konstruktorów , podobnie jak Java, rzucanie wyjątków jest znacznie mniej uciążliwe ”. - destruktory muszą być uruchamiane przy wychodzeniu z zakresu normalnie, a finallybloki Java nie różnią się niczym od destruktorów C ++ pod względem obciążenia implementacyjnego.
Pavel Minaev

Podoba mi się ta odpowiedź, ale podobnie jak Pavel nie sądzę, aby druga kwestia była uzasadniona. Gdy zakres kończy się z jakiegokolwiek powodu, w tym innych typów obsługi błędów lub po prostu kontynuowania programu, i tak zostaną wywołane destruktory.
Catskul

+1 za wymienienie kultury języka jako powodu, aby iść z prądem. Ta odpowiedź jest dla niektórych niezadowalająca, ale jest to prawdziwy powód (i uważam, że jest to najbardziej dokładny powód).
j_random_hacker

@Pavel: Proszę zignorować mój niepoprawny komentarz powyżej (teraz usunięty), który mówi, że finallybloki Javy nie są gwarantowane - oczywiście, że tak. Byłem mylony z finalize()metodą Java , której uruchomienie nie jest gwarantowane.
j_random_hacker

Patrząc wstecz na tę odpowiedź, problem z destruktorami polega na tym, że wszystkie muszą być wywoływane w odpowiednim momencie, a nie potencjalnie rozstawiane z każdym powrotem funkcji. To wciąż nie jest dobry powód, ale wydaje mi się, że ma to trochę sensu.
David Thornley,

2

To jest zły przykład użycia wyjątków jako przepływu sterowania:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   try {
      totalIncome= calculateIncomeAsTypeA();
   } catch (IncorrectIncomeTypeException& e) {
      totalIncome= calculateIncomeAsTypeB();
   }

   return totalIncome;
}

Co jest bardzo złe, ale powinieneś pisać:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   if (incomeType == A) {
      totalIncome= calculateIncomeAsTypeA();
   } else if (incomeType == B) {
      totalIncome= calculateIncomeAsTypeB();
   }
   return totalIncome;
}

Ten drugi przykład oczywiście wymaga pewnych refaktoryzacji (jak użycie strategii wzorca projektowego), ale dobrze ilustruje, że wyjątki nie są przeznaczone do sterowania przepływem.

Wyjątki wiążą się również z pewnymi ograniczeniami wydajności, ale problemy z wydajnością powinny być zgodne z zasadą: „przedwczesna optymalizacja jest źródłem wszelkiego zła”


Wygląda na przypadek, w którym więcej polimorfizmu byłoby dobre, zamiast sprawdzania incomeType.
Sarah Vessels

@Sarah tak, wiem, że byłoby dobrze. Jest tutaj tylko w celach ilustracyjnych.
Edison Gustavo Muenz

3
Czemu to jest złe? Dlaczego powinieneś pisać to w drugi sposób? Pytający chce poznać powody reguł, a nie zasady. -1.
j_random_hacker

2
  1. Łatwość utrzymania: jak wspomniano powyżej, rzucanie wyjątków w mgnieniu oka jest podobne do używania goto.
  2. Interoperacyjność: nie możesz łączyć bibliotek C ++ z modułami C / Python (przynajmniej nie jest to łatwe), jeśli używasz wyjątków.
  3. Spadek wydajności: RTTI służy do faktycznego znajdowania typu wyjątku, który nakłada dodatkowe obciążenie. Dlatego wyjątki nie są odpowiednie do obsługi często występujących przypadków użycia (użytkownik wprowadzony przez użytkownika zamiast ciągu itp.).

2
1. Ale w niektórych językach wyjątki rzucane w mgnieniu oka. 3. Czasami wydajność nie ma znaczenia. (80% czasu?)
UncleBens

2
2. Programy C ++ prawie zawsze używają wyjątków, ponieważ wiele z nich korzysta z bibliotek standardowych. Klasy kontenera rzucają. Klasa string rzuca. Rzut klas strumienia.
David Thornley

Jakie operacje na kontenerach i łańcuchach są generowane, z wyjątkiem at()? To powiedziawszy, każdy newmoże rzucić, więc ...
Pavel Minaev

O ile rozumiem, RTTI nie jest bezwzględnie konieczne do Try / Throw / Catch. Zawsze wspominano o pogorszeniu wydajności i wierzę w to, ale nikt nigdy nie wspomina o skali ani linku do jakichkolwiek odniesień.
Catskul

UncleBens: (1) dotyczy konserwacji, a nie wydajności. Nadmierne użycie próby / złapania / wyrzucenia zmniejsza czytelność kodu. Praktyczna zasada (i mogę zostać z tego powodu ostrzelana, ale to tylko moja opinia), im mniej punktów wejścia i wyjścia, tym łatwiej jest odczytać kod. David Thornley: Nie wtedy, gdy potrzebujesz interoperacyjności. Flaga -fno-exceptions w gcc wyłącza wyjątki, kiedy kompilujesz bibliotekę wywoływaną z kodu C.
Sridhar Iyer

2

Powiedziałbym, że wyjątki to mechanizm, który pozwala w bezpieczny sposób wydostać się z bieżącego kontekstu (z bieżącej ramki stosu w najprostszym sensie, ale to coś więcej). Jest to najbliższa rzecz, jaką osiągnęło programowanie strukturalne. Aby używać wyjątków w sposób, w jaki były przeznaczone, musisz mieć sytuację, w której nie możesz kontynuować tego, co robisz teraz, i nie możesz sobie z tym poradzić w miejscu, w którym się znajdujesz. Na przykład, jeśli hasło użytkownika jest nieprawidłowe, możesz kontynuować, zwracając wartość false. Ale jeśli podsystem interfejsu użytkownika zgłasza, że ​​nie może nawet monitować użytkownika, zwykłe zwrócenie komunikatu „logowanie nie powiodło się” byłoby błędne. Obecny poziom kodu po prostu nie wie, co robić. Dlatego używa mechanizmu wyjątków, aby delegować odpowiedzialność na kogoś powyżej, kto może wiedzieć, co robić.


2

Jednym z bardzo praktycznych powodów jest to, że podczas debugowania programu często włączam Wyjątki pierwszej szansy (Debuguj -> Wyjątki), aby debugować aplikację. Jeśli zdarza się wiele wyjątków, bardzo trudno jest stwierdzić, gdzie coś poszło „nie tak”.

Prowadzi to również do pewnych anty-wzorców, takich jak niesławny „rzut łapą”, i zaciemnia rzeczywiste problemy. Więcej informacji na ten temat można znaleźć we wpisie na blogu, który opublikowałem na ten temat.


2

Wolę jak najmniej używać wyjątków. Wyjątki zmuszają programistę do obsługi pewnych warunków, które mogą, ale nie muszą być rzeczywistym błędem. Określenie, czy dany wyjątek jest problemem krytycznym, czy też problemem, który należy natychmiast rozwiązać.

Kontrargumentem jest to, że leniwi ludzie muszą pisać więcej, aby strzelić sobie w stopy.

Polityka kodowania Google mówi, aby nigdy nie używać wyjątków , szczególnie w C ++. Twoja aplikacja albo nie jest przygotowana do obsługi wyjątków, albo jest. Jeśli tak nie jest, wyjątek prawdopodobnie będzie go propagował, aż aplikacja umrze.

Znalezienie jakiejś biblioteki, z której korzystałeś, nigdy nie jest zabawne, rzuca wyjątki i nie byłeś przygotowany do ich obsługi.


1

Uzasadniony przypadek zgłoszenia wyjątku:

  • Próbujesz otworzyć plik, ale go tam nie ma, generowany jest wyjątek FileNotFoundException;

Nieuzasadniona sprawa:

  • Chcesz coś zrobić tylko wtedy, gdy plik nie istnieje, próbujesz otworzyć plik, a następnie dodać kod do bloku catch.

Używam wyjątków, gdy chcę przerwać przepływ aplikacji do pewnego momentu . W tym miejscu znajduje się haczyk (...) dla tego wyjątku. Na przykład bardzo często musimy przetwarzać wiele projektów, a każdy projekt powinien być przetwarzany niezależnie od pozostałych. Zatem pętla przetwarzająca projekty ma blok try ... catch, a jeśli podczas przetwarzania projektu zostanie zgłoszony wyjątek, wszystko dla tego projektu jest wycofywane, błąd jest rejestrowany, a następny projekt jest przetwarzany. Życie toczy się dalej.

Myślę, że powinieneś używać wyjątków dla rzeczy takich jak plik, który nie istnieje, wyrażenie, które jest nieprawidłowe i tym podobne. Nie powinieneś używać wyjątków do testowania zakresu / testowania typów danych / istnienia plików / czegokolwiek innego, jeśli istnieje łatwa / tania alternatywa dla tego. Nie powinieneś używać wyjątków do testowania zakresu / testowania typu danych / istnienia pliku / czegokolwiek innego, jeśli istnieje łatwa / tania alternatywa dla tego, ponieważ ten rodzaj logiki sprawia, że ​​kod jest trudny do zrozumienia:

RecordIterator<MyObject> ri = createRecordIterator();
try {
   MyObject myobject = ri.next();
} catch(NoSuchElement exception) {
   // Object doesn't exist, will create it
}

Tak byłoby lepiej:

RecordIterator<MyObject> ri = createRecordIterator();
if (ri.hasNext()) {
   // It exists! 
   MyObject myobject = ri.next();
} else {
   // Object doesn't exist, will create it
}

KOMENTARZ DO ODPOWIEDZI:

Może mój przykład nie był zbyt dobry - ri.next () nie powinien zgłaszać wyjątku w drugim przykładzie, a jeśli tak, to jest coś naprawdę wyjątkowego i należy podjąć inną akcję w innym miejscu. Gdy przykład 1 jest intensywnie używany, programiści wychwycą ogólny wyjątek zamiast konkretnego i założą, że wyjątek wynika z błędu, którego się spodziewają, ale może to być coś innego. Ostatecznie prowadzi to do ignorowania rzeczywistych wyjątków, ponieważ wyjątki stały się częścią przepływu aplikacji, a nie wyjątkiem od niego.

Komentarze na ten temat mogą dodać więcej niż sama moja odpowiedź.


1
Dlaczego nie? Dlaczego nie zastosować wyjątków w drugim przypadku, o którym wspomniałeś? Pytający chce poznać powody wprowadzenia reguł, a nie zasady.
j_random_hacker

Dzięki za rozwinięcie, ale IMHO twoje 2 fragmenty kodu mają prawie identyczną złożoność - oba używają wysoce zlokalizowanej logiki sterowania. Tam, gdzie złożoność wyjątków najwyraźniej przekracza złożoność tego / wtedy / else, gdy w bloku try znajduje się kilka instrukcji, z których każde może zostać rzucone - czy zgodzisz się?
j_random_hacker

Może mój przykład nie był zbyt dobry - funkcja ri.next () nie powinna zgłaszać wyjątku w drugim przykładzie, a jeśli tak, to jest coś naprawdę wyjątkowego i należy podjąć inną akcję w innym miejscu. Gdy przykład 1 jest intensywnie używany, programiści wychwycą ogólny wyjątek zamiast konkretnego i założą, że wyjątek wynika z oczekiwanego przez nich błędu, ale może to być coś innego. Ostatecznie prowadzi to do ignorowania rzeczywistych wyjątków, ponieważ wyjątki stały się częścią przepływu aplikacji, a nie wyjątkiem od niego.
Ravi Wallau

Więc mówisz: z czasem inne stwierdzenia mogą narastać wewnątrz trybloku i wtedy nie jesteś już pewien, czy catchblok naprawdę łapie to, co myślałeś, że łapie - czy to prawda? Chociaż trudniej jest nadużywać podejścia if / then / else w ten sam sposób, ponieważ można testować tylko jedną rzecz naraz, a nie zestaw rzeczy naraz, więc wyjątkowe podejście może prowadzić do bardziej delikatnego kodu. Jeśli tak, omów to w swojej odpowiedzi, a ja z radością daję +1, ponieważ uważam, że kruchość kodu jest prawdziwym powodem.
j_random_hacker

0

Zasadniczo wyjątki są nieustrukturyzowaną i trudną do zrozumienia formą kontroli przepływu. Jest to konieczne, gdy mamy do czynienia z warunkami błędu, które nie są częścią normalnego przepływu programu, aby uniknąć zbytniego zaśmiecania logiki obsługi błędów normalnego sterowania przepływem kodu.

Wyjątki IMHO powinny być używane, gdy chcesz zapewnić rozsądną wartość domyślną w przypadku, gdy wywołujący zaniedbuje napisanie kodu obsługi błędów lub jeśli błąd może być najlepiej obsłużony wyżej na stosie wywołań niż bezpośredni wywołujący. Rozsądnym domyślnym rozwiązaniem jest wyjście z programu i wyświetlenie rozsądnego komunikatu o błędzie diagnostycznym. Szalona alternatywa polega na tym, że program kuleje dalej w błędnym stanie i ulega awarii lub po cichu generuje złe wyniki w późniejszym, trudniejszym do zdiagnozowania punkcie. Jeśli „błąd” jest wystarczającą normalną częścią przepływu programu, której wywołujący nie mógł zapomnieć o sprawdzeniu go, to wyjątków nie należy używać.


0

Myślę, że „używaj tego rzadko” nie jest właściwym zdaniem. Wolałbym „rzucać tylko w wyjątkowych sytuacjach”.

Wiele osób wyjaśniło, dlaczego wyjątki nie powinny być stosowane w normalnych sytuacjach. Wyjątki mają swoje prawo do obsługi błędów i wyłącznie do obsługi błędów.

Skoncentruję się na innym punkcie:

Inną rzeczą jest kwestia wydajności. Kompilatory długo walczyły, aby uzyskać je szybko. Nie jestem pewien, jaki jest teraz dokładny stan, ale jeśli użyjesz wyjątków dla przepływu sterowania, będziesz miał inny problem: Twój program będzie spowolniony!

Powodem jest to, że wyjątki to nie tylko bardzo potężne instrukcje goto, ale także rozwijanie stosu dla wszystkich opuszczanych ramek. Zatem implicite również obiekty na stosie muszą zostać zdekonstruowane i tak dalej. Więc nie zdając sobie z tego sprawy, jedno rzucenie wyjątku naprawdę pociągnie za sobą całą masę mechaniki. Procesor będzie musiał zrobić bardzo dużo.

Więc skończysz, elegancko spalając procesor bez wiedzy.

A więc: używaj wyjątków tylko w wyjątkowych przypadkach - Znaczenie: gdy wystąpiły prawdziwe błędy!


1
„Powodem jest to, że wyjątki to nie tylko bardzo potężne instrukcje goto, ale także rozwijanie stosu dla wszystkich ramek, które opuszczają. Zatem domyślnie również obiekty na stosie muszą zostać zdekonstruowane i tak dalej”. - jeśli stracisz kilka ramek stosu w stosie wywołań, to wszystkie te ramki stosu (i obiekty na nich) i tak będą musiały zostać ostatecznie zniszczone - nie ma znaczenia, czy dzieje się tak, ponieważ wracasz normalnie, czy też rzucasz wyjątek. W końcu, poza wezwaniem std::terminate(), nadal musisz zniszczyć.
Pavel Minaev

Masz oczywiście rację. Pamiętaj jednak: za każdym razem, gdy korzystasz z wyjątków, system musi zapewnić infrastrukturę, aby wykonać to wszystko w magiczny sposób. Chciałem tylko trochę wyjaśnić, że to nie tylko goto. Również podczas odwijania system musi znaleźć odpowiednie miejsce łapania, co będzie kosztowało dodatkowy czas, kod i użycie RTTI. Jest to więc znacznie więcej niż skok - większość ludzi po prostu tego nie rozumie.
Juergen

Dopóki trzymasz się C ++ zgodnego ze standardami, RTTI jest nieuniknione. W przeciwnym razie oczywiście jest narzut. Tyle, że często widzę to znacznie przesadzone.
Pavel Minaev

1
-1. „Wyjątki mają swoje prawo do obsługi błędów i wyłącznie do obsługi błędów” - mówi kto? Czemu? Wydajność nie może być jedynym powodem. (A) Większość kodu na świecie nie znajduje się w najbardziej wewnętrznej pętli silnika renderującego grę lub funkcji mnożenia macierzy, więc używanie wyjątków nie będzie miało zauważalnej różnicy w wydajności. (B) Jak ktoś zauważył gdzieś w komentarzu, całe to rozwijanie stosu musiałoby ostatecznie i tak nastąpić, nawet jeśli stary błąd sprawdzania-zwracania błędów w stylu C i przekazywania zwrotów w razie potrzeby zastosowano podejście obsługi.
j_random_hacker

Powiedz I. W tym wątku podano wiele powodów. Po prostu je przeczytaj. Chciałem opisać tylko jeden. Jeśli to nie ten, którego potrzebujesz, nie obwiniaj mnie.
Juergen

0

Celem wyjątków jest uodpornienie oprogramowania na błędy. Jednak konieczność zapewnienia odpowiedzi na każdy wyjątek zgłoszony przez funkcję prowadzi do pomijania. Wyjątki to tylko formalna struktura zmuszająca programistów do przyznania, że ​​pewne rzeczy mogą pójść nie tak z rutyną i że klient programista musi być świadomy tych warunków i uwzględniać je w razie potrzeby.

Szczerze mówiąc, wyjątki są dodawane do języków programowania, aby zapewnić programistom pewne formalne wymagania, które przenoszą odpowiedzialność za obsługę przypadków błędów z bezpośredniego dewelopera na przyszłego programistę.

Uważam, że dobry język programowania nie obsługuje wyjątków, jakie znamy w C ++ i Javie. Powinieneś wybrać języki programowania, które mogą zapewnić alternatywny przepływ dla wszelkiego rodzaju wartości zwracanych z funkcji. Programista powinien być odpowiedzialny za przewidywanie wszystkich form wyników procedury i obsługiwanie ich w osobnym pliku kodu, jeśli tylko mogę.


0

Używam wyjątków, jeśli:

  • wystąpił błąd, którego nie można odzyskać z lokalnego AND
  • jeśli błąd nie zostanie usunięty z programu, powinien zakończyć się.

Jeśli błąd można naprawić (użytkownik wprowadził „jabłko” zamiast liczby), a następnie napraw (ponownie poproś o dane wejściowe, zmień na wartość domyślną itp.).

Jeśli błędu nie można naprawić lokalnie, ale aplikacja może kontynuować (użytkownik próbował otworzyć plik, ale plik nie istnieje), wówczas odpowiedni jest kod błędu.

Jeśli błędu nie można odzyskać lokalnie, a aplikacji nie można kontynuować bez odzyskiwania (brakuje pamięci / miejsca na dysku / itp.), Wyjątek jest właściwą drogą.


1
-1. Przeczytaj uważnie pytanie. Większość programistów uważa, że ​​wyjątki są odpowiednie dla pewnych typów obsługi błędów lub że nigdy nie są odpowiednie - nawet nie rozważają możliwości wykorzystania ich do innych, bardziej egzotycznych form kontroli przepływu. Pytanie brzmi: dlaczego tak jest?
j_random_hacker

Powinieneś również uważnie się z nim zapoznać. Odpowiedziałem: "Jaka jest filozofia bycia wyjątkowo konserwatywnym w sposobie ich używania?" z moją filozofią stojącą za konserwatywnością w sposobie ich używania.
Bill

IMHO nie wyjaśniłeś, dlaczego konserwatyzm jest konieczny. Dlaczego czasami są tylko „odpowiednie”? Dlaczego nie przez cały czas? (
Swoją drogą,

PO zadał siedem różnych pytań. Zdecydowałem się odpowiedzieć tylko na jedno. Przykro mi, że uważasz, że warto głosować przeciw.
Bill

0

Kto powiedział, że należy ich używać konserwatywnie? Po prostu nigdy nie używaj wyjątków do kontroli przepływu i to wszystko. A kogo obchodzi koszt wyjątku, gdy jest już wyrzucony?


0

Moje dwa centy:

Lubię używać wyjątków, ponieważ pozwala mi to programować tak, jakby nie było żadnych błędów. Więc mój kod pozostaje czytelny, nie jest rozproszony przez wszelkiego rodzaju obsługę błędów. Oczywiście obsługa błędów (obsługa wyjątków) jest przenoszona na koniec (blok catch) lub jest uważana za odpowiedzialność poziomu wywołania.

Świetnym przykładem dla mnie jest obsługa plików lub obsługa baz danych. Załóżmy, że wszystko jest w porządku i zamknij plik na końcu lub jeśli wystąpi wyjątek. Lub wycofaj swoją transakcję, gdy wystąpi wyjątek.

Problem z wyjątkami polega na tym, że szybko robi się bardzo rozwlekły. Chociaż miało to pozwolić Twojemu kodowi pozostać bardzo czytelnym i po prostu skupić się na normalnym przepływie rzeczy, ale jeśli jest używane konsekwentnie, prawie każde wywołanie funkcji musi być opakowane w blok try / catch i zaczyna się to udawać.

W przypadku ParseInt, jak wspomniano wcześniej, podoba mi się idea wyjątków. Po prostu zwróć wartość. Jeśli parametr nie był analizowalny, zgłoś wyjątek. Z jednej strony sprawia, że ​​kod jest czystszy. Na poziomie dzwonienia musisz zrobić coś takiego

try 
{
   b = ParseInt(some_read_string);
} 
catch (ParseIntException &e)
{
   // use some default value instead
   b = 0;
}

Kod jest czysty. Kiedy otrzymuję rozproszony ParseInt w ten sposób, tworzę funkcje opakowania, które obsługują wyjątki i zwracają mi domyślne wartości. Na przykład

int ParseIntWithDefault(String stringToConvert, int default_value=0)
{
   int result = default_value;
   try
   {
     result = ParseInt(stringToConvert);
   }
   catch (ParseIntException &e) {}

   return result;
}

Podsumowując: to, co przegapiłem w dyskusji, to fakt, że wyjątki pozwalają mi uczynić mój kod łatwiejszym / bardziej czytelnym, ponieważ mogę bardziej zignorować warunki błędu. Problemy:

  • wyjątki nadal trzeba gdzieś załatwić. Dodatkowy problem: c ++ nie ma składni, która pozwala określić, które wyjątki funkcja może zgłosić (tak jak robi to java). Tak więc poziom wywołania nie jest świadomy, które wyjątki mogą wymagać obsługi.
  • czasami kod może być bardzo rozwlekły, jeśli każda funkcja musi być opakowana w blok try / catch. Ale czasami to nadal ma sens.

Dlatego czasami trudno jest znaleźć dobrą równowagę.


-1

Przykro mi, ale odpowiedź brzmi: „nie bez powodu nazywa się je wyjątkami”. To wyjaśnienie jest „praktyczną zasadą”. Nie można podać pełnego zestawu okoliczności, w których wyjątki powinny lub nie powinny być używane, ponieważ to, czym jest wyjątek krytyczny (definicja w języku angielskim) dla jednej problematycznej domeny, jest normalną procedurą operacyjną dla innej problematycznej domeny. Praktyczne zasady nie są przeznaczone do ślepego przestrzegania. Zamiast tego mają na celu pokierowanie poszukiwaniem rozwiązania. „Nie bez powodu są one nazywane wyjątkami” oznacza, że ​​powinieneś wcześniej określić, jaki jest normalny błąd, z którym dzwoniący może sobie poradzić, i z jaką niezwykłą sytuacją dzwoniący nie może sobie poradzić bez specjalnego kodowania (bloków catch).

Prawie każda reguła programowania jest naprawdę wskazówką mówiącą „Nie rób tego, chyba że masz naprawdę dobry powód”: „Nigdy nie używaj goto”, „Unikaj zmiennych globalnych”, „Wyrażenia regularne zwiększają liczbę problemów o jeden "itp. Wyjątki nie są wyjątkiem ....


... a pytający chciałby wiedzieć, dlaczego jest to praktyczna zasada, zamiast usłyszeć (jeszcze raz), że jest to praktyczna reguła. -1.
j_random_hacker

Przyznałem to w mojej odpowiedzi. Nie ma wyraźnego powodu. Praktyczne zasady są niejasne z definicji. Gdyby istniał wyraźny powód, byłaby to reguła, a nie praktyczna reguła. Każde wyjaśnienie w każdej innej odpowiedzi powyżej zawiera zastrzeżenia, więc nie wyjaśniają one również dlaczego.
jmucchiello

Może nie być ostatecznego „dlaczego”, ale są częściowe „dlaczego”, o których wspominają inni, np. „Ponieważ tak robią wszyscy inni” (IMHO to smutny, ale prawdziwy powód) lub „wydajność” (IMHO ten powód jest zwykle przesadzony , ale to jednak powód). To samo dotyczy innych praktycznych reguł, takich jak unikanie goto (zwykle komplikuje to analizę przepływu sterowania bardziej niż równoważna pętla) i unikanie zmiennych globalnych (potencjalnie wprowadzają dużo sprzężeń, utrudniając późniejsze zmiany kodu i zwykle to samo cele można osiągnąć przy mniejszym łączeniu innych sposobów).
j_random_hacker

A wszystkie te powody mają długą listę zastrzeżeń, na które była moja odpowiedź. Nie ma prawdziwego powodu poza szerokim doświadczeniem w programowaniu. Istnieje kilka praktycznych zasad, które wykraczają poza uzasadnienie. Ta sama praktyczna zasada może spowodować, że „eksperci” nie będą zgodni co do tego, dlaczego. Ty sam traktujesz „wydajność” z przymrużeniem oka. To byłby mój szczyt listy. Analiza kontroli przepływu nawet się ze mną nie rejestruje, ponieważ (jak „no goto”) uważam, że problemy z kontrolą przepływu są przesadzone. Zwrócę również uwagę, że moja odpowiedź jest próbą wyjaśnienia, JAK używasz „oklepanej” odpowiedzi.
jmucchiello

Zgadzam się z tobą, o ile wszystkie praktyczne zasady mają długą listę zastrzeżeń. Nie zgadzam się z tym, że uważam, że warto spróbować zidentyfikować pierwotne powody reguły, a także konkretne zastrzeżenia. (Mam na myśli idealny świat, w którym mamy nieskończony czas na zastanowienie się nad tymi rzeczami i oczywiście bez terminów;)) Myślę, że o to prosił PO.
j_random_hacker
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.