Moje pytanie brzmi: co preferuje większość programistów do obsługi błędów, wyjątków lub kodów zwrotnych błędów. Podaj konkretny język (lub rodzinę języków) i dlaczego wolisz jedno od drugiego.
Pytam z ciekawości. Osobiście wolę kody zwrotne błędów, ponieważ są mniej wybuchowe i nie zmuszają kodu użytkownika do płacenia kary za wydajność wyjątku, jeśli nie chcą.
aktualizacja: dzięki za wszystkie odpowiedzi! Muszę powiedzieć, że chociaż nie lubię nieprzewidywalności przepływu kodu z wyjątkami. Odpowiedź dotycząca kodu powrotu (i uchwytów ich starszego brata) dodaje dużo szumu do kodu.
language-agnostic
exception
Robert Gould
źródło
źródło
Odpowiedzi:
W przypadku niektórych języków (np. C ++) wyciek zasobów nie powinien być przyczyną
C ++ jest oparty na RAII.
Jeśli masz kod, który może zawieść, zwrócić lub wyrzucić (czyli większość normalnego kodu), to powinieneś mieć swój wskaźnik zawinięty wewnątrz inteligentnego wskaźnika (zakładając, że masz bardzo dobry powód, aby nie tworzyć obiektu na stosie).
Kody zwrotne są bardziej szczegółowe
Są gadatliwe i mają tendencję do przekształcania się w coś takiego:
W końcu twój kod jest zbiorem zidentyfikowanych instrukcji (widziałem ten rodzaj kodu w kodzie produkcyjnym).
Ten kod można z powodzeniem przetłumaczyć na:
Który wyraźnie oddziela kod i przetwarzanie błędów, co może być dobrą rzeczą.
Kody zwrotne są bardziej kruche
Jeśli nie jakieś niejasne ostrzeżenie z jednego kompilatora (zobacz komentarz "phjr"), można je łatwo zignorować.
W powyższych przykładach załóżmy, że ktoś zapomni zająć się możliwym błędem (to się dzieje ...). Błąd jest ignorowany, gdy „zwracany”, i prawdopodobnie eksploduje później (tj. Wskaźnik NULL). Ten sam problem nie wystąpi z wyjątkiem.
Błąd nie zostanie zignorowany. Czasami jednak chcesz, żeby nie wybuchło ... Więc musisz ostrożnie wybierać.
Czasami kody zwrotne muszą być przetłumaczone
Powiedzmy, że mamy następujące funkcje:
Jaki jest typ zwrotu funkcji doTryToDoSomethingWithAllThisMess, jeśli jedna z wywoływanych funkcji nie powiedzie się?
Kody zwrotne nie są uniwersalnym rozwiązaniem
Operatorzy nie mogą zwrócić kodu błędu. Konstruktorzy C ++ też nie mogą.
Kody zwrotne oznaczają, że nie możesz łączyć wyrażeń
Następstwem powyższego punktu. A jeśli chcę napisać:
Nie mogę, ponieważ wartość zwracana jest już używana (a czasami nie można jej zmienić). Zatem wartość zwracana staje się pierwszym parametrem, wysyłanym jako odniesienie ... Lub nie.
Wyjątki są wpisywane
Możesz wysłać różne klasy dla każdego rodzaju wyjątku. Wyjątki zasobów (tj. Brak pamięci) powinny być lekkie, ale wszystko inne może być tak ciężkie, jak to konieczne (podoba mi się wyjątek Java, który daje mi cały stos).
Każdy połów można następnie wyspecjalizować.
Nigdy nie używaj catch (...) bez ponownego rzucenia
Zwykle nie powinieneś ukrywać błędu. Jeśli nie wyrzucisz ponownie, przynajmniej zapisz błąd w pliku, otwórz skrzynkę wiadomości, cokolwiek ...
Wyjątkiem są ... NUKE
Jedynym problemem jest to, że ich nadużywanie spowoduje powstanie kodu pełnego prób / przechwyceń. Ale problem jest gdzie indziej: kto próbuje / przechwytuje jego kod za pomocą kontenera STL? Mimo to te kontenery mogą wysłać wyjątek.
Oczywiście w C ++ nigdy nie pozwól wyjątkowi wyjść z destruktora.
Wyjątki są ... synchroniczne
Pamiętaj, aby złapać je, zanim wyciągną twój wątek na kolanach lub rozprzestrzenią się w pętli wiadomości systemu Windows.
Rozwiązaniem może być ich zmieszanie?
Więc myślę, że rozwiązaniem jest rzucenie, gdy coś nie powinno się wydarzyć. A kiedy coś może się zdarzyć, użyj kodu powrotu lub parametru, aby umożliwić użytkownikowi reakcję na to.
Tak więc jedyne pytanie brzmi: „co jest czymś, co nie powinno się wydarzyć?”
To zależy od umowy pełnionej funkcji. Jeśli funkcja akceptuje wskaźnik, ale określa, że wskaźnik nie może mieć wartości NULL, wówczas można zgłosić wyjątek, gdy użytkownik wyśle wskaźnik NULL (pytanie brzmi, w C ++, gdy autor funkcji nie używa odwołań wskazówek, ale ...)
Innym rozwiązaniem byłoby pokazanie błędu
Czasami twój problem polega na tym, że nie chcesz błędów. Używanie wyjątków lub kodów powrotu błędów jest fajne, ale ... Chcesz o tym wiedzieć.
W mojej pracy używamy swego rodzaju „Assert”. W zależności od wartości pliku konfiguracyjnego, niezależnie od opcji debugowania / kompilacji wydania:
Zarówno w przypadku programowania, jak i testowania umożliwia to użytkownikowi dokładne wskazanie problemu w momencie jego wykrycia, a nie po (gdy jakiś kod dba o zwracaną wartość lub wewnątrz zaczepu).
Dodawanie do starszego kodu jest łatwe. Na przykład:
prowadzi kod podobny do:
(Mam podobne makra, które są aktywne tylko podczas debugowania).
Zwróć uwagę, że na produkcji plik konfiguracyjny nie istnieje, więc klient nigdy nie widzi wyniku działania tego makra ... Ale w razie potrzeby łatwo go aktywować.
Wniosek
Kiedy kodujesz za pomocą kodów powrotnych, przygotowujesz się na niepowodzenie i masz nadzieję, że Twoja forteca testów jest wystarczająco bezpieczna.
Kiedy kodujesz za pomocą wyjątku, wiesz, że Twój kod może się nie powieść i zazwyczaj umieszczasz w swoim kodzie pułapkę przeciwpożarową w wybranym strategicznym miejscu. Ale zazwyczaj w twoim kodzie jest bardziej „co musi zrobić”, a potem „czego się boję, że się wydarzy”.
Ale kiedy w ogóle kodujesz, musisz użyć najlepszego dostępnego narzędzia, a czasami jest to „Nigdy nie ukrywaj błędu i pokaż go tak szybko, jak to możliwe”. Makro, o którym mówiłem powyżej, jest zgodne z tą filozofią.
źródło
if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
doSomething(); doSomethingElse(); ...
lepiej, ponieważ jeśli muszę dodać if / while / etc. instrukcje do normalnego wykonywania, nie chcę, aby były mieszane z if / while / etc. instrukcje dodane w wyjątkowych celach ... A ponieważ prawdziwa zasada dotycząca używania wyjątków polega na rzucaniu , a nie łapaniu , instrukcje try / catch zwykle nie są inwazyjne.Właściwie używam obu.
Używam kodów powrotu, jeśli jest to znany, możliwy błąd. Jeśli jest to scenariusz, o którym wiem, że może i się wydarzy, to jest kod, który zostanie odesłany.
Wyjątki są używane wyłącznie do rzeczy, których NIE oczekuję.
źródło
Zgodnie z rozdziałem 7 zatytułowanym „Wyjątki” w wytycznych dotyczących projektowania ram: konwencje, idiomy i wzorce dla bibliotek wielokrotnego użytku .NET podano wiele uzasadnień, dlaczego używanie wyjątków zamiast zwracanych wartości jest konieczne w przypadku struktur obiektowych, takich jak C #.
Być może jest to najważniejszy powód (strona 179):
„Wyjątki dobrze integrują się z językami zorientowanymi obiektowo. Języki zorientowane obiektowo mają tendencję do nakładania ograniczeń na sygnatury elementów członkowskich, które nie są narzucane przez funkcje w językach innych niż OO. Na przykład w przypadku konstruktorów, przeciążeń operatorów i właściwości programista nie ma wyboru w zakresie zwracanej wartości. Z tego powodu nie można ustandaryzować raportowania błędów opartego na wartości zwracanej dla struktur zorientowanych obiektowo. Metoda raportowania błędów, taka jak wyjątki, jest poza pasmem sygnatury metody jest jedyną opcją ”.
źródło
Preferuję (w C ++ i Pythonie) używanie wyjątków. Funkcje dostępne w języku sprawiają, że jest to dobrze zdefiniowany proces zarówno do zgłaszania, wychwytywania i (jeśli to konieczne) ponownego zgłaszania wyjątków, dzięki czemu model jest łatwy do zobaczenia i użycia. Koncepcyjnie jest bardziej przejrzysty niż kody powrotne, ponieważ określone wyjątki można zdefiniować za pomocą ich nazw i dołączyć do nich dodatkowe informacje. W przypadku kodu powrotu jesteś ograniczony tylko do wartości błędu (chyba że chcesz zdefiniować obiekt ReturnStatus lub coś w tym rodzaju).
O ile kod, który piszesz, nie jest krytyczny czasowo, obciążenie związane z rozwijaniem stosu nie jest wystarczająco znaczące, aby się o niego martwić.
źródło
Wyjątki powinny być zwracane tylko wtedy, gdy dzieje się coś, czego się nie spodziewałeś.
Innym punktem wyjątków, historycznie, jest to, że kody powrotu są z natury zastrzeżone, czasami funkcja C może zwracać 0, aby wskazać sukces, czasami -1 lub jeden z nich w przypadku niepowodzenia z 1 dla sukcesu. Nawet jeśli są wyliczane, wyliczenia mogą być niejednoznaczne.
Wyjątki mogą również dostarczyć o wiele więcej informacji, a konkretnie dobrze opisują „Coś poszło nie tak, oto co, ślad stosu i kilka dodatkowych informacji dotyczących kontekstu”
Biorąc to pod uwagę, dobrze wyliczony kod powrotu może być przydatny w przypadku znanego zestawu wyników, prostego „wyników funkcji i po prostu działa w ten sposób”
źródło
W Javie używam (w następującej kolejności):
Projektowanie według kontraktu (zapewnienie spełnienia warunków wstępnych przed wypróbowaniem czegokolwiek , co może się nie powieść). To łapie większość rzeczy i zwracam w tym celu kod błędu.
Zwracanie kodów błędów podczas przetwarzania pracy (i wykonywanie wycofywania w razie potrzeby).
Wyjątki, ale te są używane tylko do nieoczekiwanych rzeczy.
źródło
assert
rzeczy C , ponieważ nie wykryje problemów w kodzie produkcyjnym, chyba że będziesz uruchamiać z asercjami przez cały czas. Zwykle postrzegam asercje jako sposób na wychwycenie problemów podczas programowania, ale tylko w przypadku rzeczy, które będą potwierdzać lub nie potwierdzać konsekwentnie (takie jak rzeczy ze stałymi, a nie rzeczy ze zmiennymi lub cokolwiek innego, co może się zmienić w czasie wykonywania).Kody zwrotne prawie za każdym razem nie przechodzą testu „ Pit of Success ”.
Jeśli chodzi o wydajność:
źródło
Świetna rada, jaką otrzymałem od The Pragmatic Programmer, brzmiała następująco: „Twój program powinien być w stanie wykonywać wszystkie swoje główne funkcje bez używania żadnych wyjątków”.
źródło
Niedawno napisałem na ten temat wpis na blogu .
Narzut wydajności związany z rzucaniem wyjątku nie powinien odgrywać żadnej roli przy podejmowaniu decyzji. W końcu, jeśli robisz to dobrze, wyjątek jest wyjątkowy .
źródło
Nie lubię kodów powrotnych, ponieważ powodują, że następujący wzorzec rośnie w całym kodzie
CRetType obReturn = CODE_SUCCESS; obReturn = CallMyFunctionWhichReturnsCodes(); if (obReturn == CODE_BLOW_UP) { // bail out goto FunctionExit; }
Wkrótce wywołanie metody składające się z 4 wywołań funkcji rozrasta się z 12 wierszami obsługi błędów. Niektóre z nich nigdy się nie wydarzy. Jeśli i przełączniki obfitują.
Wyjątki są czystsze, jeśli są dobrze używane ... do sygnalizowania wyjątkowych zdarzeń ... po których ścieżka wykonania nie może być kontynuowana. Często są one bardziej opisowe i informacyjne niż kody błędów.
Jeśli masz wiele stanów po wywołaniu metody, które powinny być obsługiwane inaczej (i nie są to wyjątkowe przypadki), użyj kodów błędów lub out params. Chociaż Personaly uważam, że jest to rzadkie ...
Trochę szukałem kontrargumentu „obniżenia wydajności” ... więcej w świecie C ++ / COM, ale w nowszych językach, myślę, że różnica nie jest tak duża. W każdym razie, gdy coś wybucha, problemy z wydajnością spadają na drugi plan :)
źródło
Z każdym przyzwoitym kompilatorem lub środowiskiem wykonawczym wyjątki nie pociągają za sobą znaczącej kary. To mniej więcej jak instrukcja GOTO, która przeskakuje do procedury obsługi wyjątków. Ponadto wyjątki wychwytywane przez środowisko wykonawcze (takie jak JVM) znacznie ułatwiają izolowanie i naprawianie błędów. W dowolnym dniu wezmę wyjątek NullPointerException w Javie na segfault w C.
źródło
finally
blokami i destruktorami obiektów przydzielonych na stosie.Mam prosty zestaw reguł:
1) Używaj kodów zwrotnych w przypadku rzeczy, na które Twój rozmówca powinien zareagować.
2) Używaj wyjątków dla błędów, które mają szerszy zakres i można się spodziewać, że będą obsługiwane przez coś o wiele poziomów powyżej wywołującego, aby świadomość błędu nie musiała przenikać przez wiele warstw, co czyni kod bardziej złożonym.
W Javie zawsze używałem tylko niezaznaczonych wyjątków, sprawdzone wyjątki są po prostu kolejną formą kodu zwrotnego iz mojego doświadczenia wynika, że dwoistość tego, co może być „zwrócone” przez wywołanie metody, była generalnie bardziej utrudnieniem niż pomocą.
źródło
Używam wyjątków w Pythonie zarówno w wyjątkowych, jak i nie wyjątkowych okolicznościach.
Często dobrze jest móc użyć wyjątku, aby wskazać, że „żądanie nie mogło zostać wykonane”, w przeciwieństwie do zwracania wartości błędu. Oznacza to, że / zawsze / wiesz, że wartość zwracana jest właściwego typu, zamiast arbitralnie None lub NotFoundSingleton czy coś takiego. Oto dobry przykład, w którym wolę używać procedury obsługi wyjątków zamiast warunku zwracanej wartości.
Efektem ubocznym jest to, że po uruchomieniu datastore.fetch (obj_id) nigdy nie musisz sprawdzać, czy jego wartość zwracana to None, natychmiast otrzymasz ten błąd za darmo. Jest to sprzeczne z argumentem „Twój program powinien być w stanie wykonywać wszystkie swoje główne funkcje bez używania wyjątków w ogóle”.
Oto kolejny przykład sytuacji, w których wyjątki są „wyjątkowo” przydatne, aby napisać kod do obsługi systemu plików, który nie podlega warunkom wyścigu.
Jedno wywołanie systemowe zamiast dwóch, brak wyścigu. To kiepski przykład, ponieważ oczywiście zakończy się niepowodzeniem z błędem OSError w większej liczbie przypadków, niż katalog nie istnieje, ale jest to „wystarczająco dobre” rozwiązanie w wielu ściśle kontrolowanych sytuacjach.
źródło
Uważam, że kody powrotu zwiększają szum kodu. Na przykład zawsze nienawidziłem wyglądu kodu COM / ATL ze względu na kody powrotne. Musiał istnieć czek HRESULT dla każdej linii kodu. Uważam, że kod powrotu błędu jest jedną ze złych decyzji podjętych przez architektów COM. Utrudnia logiczne grupowanie kodu, przez co przegląd kodu staje się trudny.
Nie jestem pewien co do porównania wydajności, gdy istnieje wyraźne sprawdzenie kodu powrotu w każdym wierszu.
źródło
Wolę używać wyjątków do obsługi błędów i zwracania wartości (lub parametrów) jako normalnego wyniku funkcji. Daje to łatwy i spójny schemat obsługi błędów, a jeśli zostanie wykonany poprawnie, sprawi, że kod będzie wyglądał lepiej.
źródło
Jedną z dużych różnic jest to, że wyjątki wymuszają obsługę błędu, podczas gdy kody powrotu błędów mogą pozostać niezaznaczone.
Kody powrotne błędów, jeśli są intensywnie używane, mogą również powodować bardzo brzydki kod z wieloma testami if podobnymi do tego formularza:
Osobiście wolę używać wyjątków dla błędów, które POWINNY lub MUSI zostać wywołane przez kod wywołujący, a kody błędów używam tylko dla „oczekiwanych błędów”, w których zwrócenie czegoś jest rzeczywiście poprawne i możliwe.
źródło
Istnieje wiele powodów, dla których warto preferować Wyjątki od kodu powrotu:
źródło
Wyjątki nie dotyczą obsługi błędów, IMO. Wyjątki są po prostu; wyjątkowe wydarzenia, których się nie spodziewałeś. Używaj ostrożnie, mówię.
Kody błędów mogą być prawidłowe, ale zwracanie 404 lub 200 z metody jest złe, IMO. Zamiast tego użyj wyliczeń (.Net), dzięki czemu kod będzie bardziej czytelny i łatwiejszy w użyciu dla innych programistów. Nie musisz też utrzymywać tabeli nad liczbami i opisami.
Również; W mojej książce wzorzec „spróbuj złapać wreszcie” jest anty-wzorcem. Próbowanie w końcu może być dobre, próba złapania może być również dobra, ale próba złapania w końcu nigdy nie jest dobra. try-last często może być zastąpiony instrukcją „using” (wzorzec IDispose), co jest lepszym rozwiązaniem IMO. Spróbuj złapać, gdzie faktycznie wyłapiesz wyjątek, z którym możesz sobie poradzić, jest dobry, lub jeśli zrobisz to:
Tak długo, jak pozwolisz wyjątkowi nadal bąbelkować, wszystko jest w porządku. Inny przykład to:
Tutaj właściwie obsługuję wyjątek; To, co robię poza try-catch, gdy metoda aktualizacji zawodzi, to inna historia, ale myślę, że moja uwaga została podjęta. :)
Dlaczego więc spróbuj złapać wreszcie anty-wzór? Dlatego:
Co się stanie, jeśli obiekt db został już zamknięty? Został zgłoszony nowy wyjątek i należy go obsłużyć! To jest lepsze:
Lub, jeśli obiekt db nie implementuje IDisposable, zrób to:
To i tak moje 2 centy! :)
źródło
Używam tylko wyjątków, żadnych kodów zwrotnych. Mówię tutaj o Javie.
Ogólna zasada, którą kieruję się jest taka, że jeśli mam wywoływaną metodę,
doFoo()
to wynika z niej, że jeśli nie "robi foo", tak jak było, to wydarzyło się coś wyjątkowego i należy wyrzucić Wyjątek.źródło
Jedną rzeczą, której boję się w przypadku wyjątków, jest to, że ich zgłoszenie zakłóci przepływ kodu. Na przykład, jeśli to zrobisz
Albo co gorsza, jeśli usunę coś, czego nie powinienem, ale zostanie wyrzucony, żeby złapać, zanim wykonam resztę czyszczenia. Rzucanie bardzo obciążało biednego użytkownika IMHO.
źródło
Moja ogólna reguła w argumencie wyjątek a kod powrotu:
źródło
Nie uważam, aby kody powrotne były mniej brzydkie niż wyjątki. Z wyjątkiem, masz
try{} catch() {} finally {}
gdzie masz kody zwrotneif(){}
. Obawiałem się wyjątków z powodów podanych w poście; nie wiesz, czy wskaźnik wymaga wyczyszczenia, co masz. Ale myślę, że masz te same problemy, jeśli chodzi o kody zwrotne. Nie znasz stanu parametrów, chyba że znasz jakieś szczegóły dotyczące danej funkcji / metody.Niezależnie od tego, jeśli to możliwe, musisz sobie poradzić z błędem. Możesz równie łatwo zezwolić na propagację wyjątku na najwyższy poziom, jak zignorować kod powrotu i pozwolić programowi na awarię.
Podoba mi się pomysł zwracania wartości (wyliczenia?) Dla wyników i wyjątku dla wyjątkowego przypadku.
źródło
W przypadku języka takiego jak Java wybrałbym wyjątek, ponieważ kompilator podaje błąd czasu kompilacji, jeśli wyjątki nie są obsługiwane, co zmusza funkcję wywołującą do obsługi / zgłaszania wyjątków.
W przypadku Pythona jestem bardziej skonfliktowany. Nie ma kompilatora, więc możliwe jest, że obiekt wywołujący nie obsługuje wyjątku zgłoszonego przez funkcję prowadzącą do wyjątków czasu wykonywania. Jeśli używasz kodów powrotu, możesz mieć nieoczekiwane zachowanie, jeśli nie zostanie prawidłowo obsłużone, a jeśli używasz wyjątków, możesz uzyskać wyjątki w czasie wykonywania.
źródło
Generalnie wolę kody zwrotne, ponieważ pozwalają dzwoniącemu zdecydować, czy awaria jest wyjątkowa .
Takie podejście jest typowe dla języka Elixir.
Ludzie wspominali, że kody powrotne mogą spowodować, że będziesz mieć wiele zagnieżdżonych
if
instrukcji, ale można to obsłużyć dzięki lepszej składni. W Elixirzewith
instrukcja ta pozwala nam łatwo oddzielić serię wartości zwracanych przez happy-path od wszelkich awarii.Elixir nadal ma funkcje, które generują wyjątki. Wracając do mojego pierwszego przykładu, mógłbym zrobić jedną z tych opcji, aby zgłosić wyjątek, jeśli nie można zapisać pliku.
Jeśli jako dzwoniący wiem, że chcę zgłosić błąd, jeśli zapis się nie powiedzie, mogę wybrać
File.write!
zamiast dzwonić połączenieFile.write
. Albo mogę zadzwonićFile.write
i zająć się każdą z możliwych przyczyn niepowodzenia w inny sposób.Oczywiście zawsze można zrobić
rescue
wyjątek, jeśli chcemy. Ale w porównaniu do obsługi informacyjnej wartości zwrotnej wydaje mi się to niezręczne. Jeśli wiem, że wywołanie funkcji może się nie powieść, a nawet powinno, to nie jest to wyjątkowy przypadek.źródło