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).
Odpowiedzi:
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.
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ć.
źródło
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.
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.
źródło
wywołania jądra (lub inne wywołania API systemu) w celu zarządzania interfejsami sygnałowymi jądra (systemowymi)
Wiele problemów związanych z tym
goto
stwierdzeniem 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.), 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?
Trudniej jest utrzymać kod, który przeskakuje wokół przetwarzania wyjątków.
źródło
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 .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.
źródło
catch
, chociaż w takim przypadku prawie równie dobrze możesz po prostu zadzwonić dostd::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”.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 „ to ”nie 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.
źródło
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.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.
źródło
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:
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:
źródło
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:
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:
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.
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
/catch
blok: 25,455 sekundKorzystanie
int.TryParse
: 1,637 milisekundW 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
/catch
blok: 29,989 sekundKorzystanie
File.Exists
: 22,820 milisekundWiele 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”.
źródło
GetNine
wykonuje 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.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.
źródło
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
catch
bloku. 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ę,f
która często wywołuje wyjątki, musimy teraz uważać, aby używać naszegotry
/catch
bloki wokół każdego wywołaniaf
. To dość złe połączenie interfejsu / implementacji.źródło
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.
źródło
Moje podejście do obsługi błędów jest takie, że istnieją trzy podstawowe typy błędów:
assert
jak). 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.
źródło
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ą:
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.
źródło
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
ParseInt
cią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
ParseInt
zawiedzie, 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.
źródło
parseInt
faktycznie 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.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ść.
źródło
finally
bloki Java nie różnią się niczym od destruktorów C ++ pod względem obciążenia implementacyjnego.finally
bloki Javy nie są gwarantowane - oczywiście, że tak. Byłem mylony zfinalize()
metodą Java , której uruchomienie nie jest gwarantowane.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”
źródło
incomeType
.źródło
at()
? To powiedziawszy, każdynew
może rzucić, więc ...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ć.
źródło
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.
źródło
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.
źródło
Uzasadniony przypadek zgłoszenia wyjątku:
Nieuzasadniona sprawa:
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ź.
źródło
try
bloku i wtedy nie jesteś już pewien, czycatch
blok 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.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ć.
źródło
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!
źródło
std::terminate()
, nadal musisz zniszczyć.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ę.
źródło
Używam wyjątków, jeśli:
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ą.
źródło
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?
źródło
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:
Dlatego czasami trudno jest znaleźć dobrą równowagę.
źródło
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 ....
źródło