Które i dlaczego wolisz kody wyjątków lub zwrotów?

92

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.

Robert Gould
źródło
1
Kura czy jajko ze świata oprogramowania… wiecznie dyskusyjne. :)
Gishu,
Przepraszam za to, ale mam nadzieję, że różne opinie pomogą ludziom (w tym mnie) dokonać właściwego wyboru.
Robert Gould,

Odpowiedzi:

107

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:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

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:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

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:

  • doSomething, które może zwrócić int o nazwie NOT_FOUND_ERROR
  • doSomethingElse, która może zwrócić wartość logiczną „false” (w przypadku niepowodzenia)
  • doSomethingElseAgain, która może zwrócić obiekt Error (z zarówno __LINE__, __FILE__, jak i połową zmiennych stosu.
  • doTryToDoSomethingWithAllThisMess, które, cóż ... Użyj powyższych funkcji i zwróć kod błędu typu ...

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ć:

CMyType o = add(a, multiply(b, c)) ;

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:

  • zarejestruj błąd
  • otwórz skrzynkę wiadomości z komunikatem „Hej, masz problem”
  • otwórz skrzynkę wiadomości z komunikatem „Hej, masz problem, czy chcesz debugować”

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:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

prowadzi kod podobny do:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(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ą.

paercebal
źródło
3
Tak, ALE jeśli chodzi o Twój pierwszy przykład, można go łatwo zapisać jako: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
bobobobo
Dlaczego nie ... Ale nadal uważam, że 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.
paercebal
1
Twoja pierwsza uwaga pokazuje, na czym polega problem z wyjątkami. Twój przepływ kontroli staje się dziwny i jest oddzielony od rzeczywistego problemu. Zastępuje niektóre poziomy identyfikacji kaskadą połowów. Użyłbym obu kodów powrotu (lub zwrócenia obiektów z dużą ilością informacji) dla możliwych błędów i wyjątków dla rzeczy, których się nie oczekuje .
Peter
2
@Peter Weber: To nie jest dziwne. Jest oddzielony od rzeczywistego problemu, ponieważ nie jest częścią normalnego przepływu wykonywania. To wyjątkowe wykonanie. I znowu, kwestia wyjątków polega na tym, aby w przypadku wyjątkowego błędu rzucać często i rzadko łapać , jeśli w ogóle. Dlatego bloki catch rzadko pojawiają się w kodzie.
paercebal,
Przykłady tego rodzaju debaty są bardzo uproszczone. Zwykle „doSomething ()” lub „doSomethingElse ()” faktycznie coś wykonuje, na przykład zmienia stan jakiegoś obiektu. Kod wyjątku nie gwarantuje przywrócenia obiektu do poprzedniego stanu, a tym bardziej, gdy catch jest bardzo daleko od wyrzutu ... Jako przykład, wyobraź sobie, że funkcja doSomething jest wywoływana dwukrotnie i zwiększa licznik przed rzuceniem. Skąd wiesz, wychwytując wyjątek, że powinieneś zmniejszyć raz lub dwa razy? Ogólnie rzecz biorąc, pisanie bezpiecznego kodu wyjątków dla czegokolwiek, co nie jest przykładem zabawki, jest bardzo trudne (niemożliwe?).
xryl669
36

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ę.

Stephen Wrighton
źródło
+1 za bardzo prosty sposób. Przynajmniej jest na tyle krótki, że mogę go bardzo szybko przeczytać. :-)
Ashish Gupta
1
Wyjątki są używane wyłącznie do rzeczy, których się NIE spodziewam. ” Jeśli ich nie oczekujesz, to po co i jak możesz ich używać?
anar khalilov
5
To, że się tego nie spodziewam, nie oznacza, że ​​nie rozumiem, jak to się stało. Oczekuję, że mój SQL Server będzie włączony i będzie odpowiadał. Ale nadal koduję swoje oczekiwania, aby móc z wdziękiem zawieść, jeśli pojawi się nieoczekiwany przestój.
Stephen Wrighton
Czy niereagujący SQL Server nie zostałby łatwo zaklasyfikowany jako „znany, możliwy błąd”?
tkburbidge
21

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ą ”.

gaj
źródło
10

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ć.

Jason Etheridge
źródło
2
Pamiętaj, że używanie wyjątków utrudnia analizę programu.
Paweł Hajdan
7

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”

johnc
źródło
6

W Javie używam (w następującej kolejności):

  1. 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.

  2. Zwracanie kodów błędów podczas przetwarzania pracy (i wykonywanie wycofywania w razie potrzeby).

  3. Wyjątki, ale te są używane tylko do nieoczekiwanych rzeczy.

paxdiablo
źródło
1
Czy nie byłoby bardziej poprawne użycie asercji do umów? Jeśli umowa zostanie zerwana, nic Cię nie uratuje.
Paweł Hajdan
@ PawełHajdan, asercje są domyślnie wyłączone, jak sądzę. Ma to ten sam problem co assertrzeczy 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).
paxdiablo
I dwanaście lat na udzielenie odpowiedzi na twoje pytanie. Powinienem poprowadzić help desk :-)
paxdiablo
6

Kody zwrotne prawie za każdym razem nie przechodzą testu „ Pit of Success ”.

  • Zbyt łatwo jest zapomnieć o sprawdzeniu kodu zwrotnego, a później pojawia się błąd „czerwonego śledzia”.
  • Kody powrotne nie zawierają żadnych wspaniałych informacji dotyczących debugowania, takich jak stos wywołań, wewnętrzne wyjątki.
  • Kody powrotne nie są propagowane, co, podobnie jak powyższy punkt, prowadzi do nadmiernego i przeplatanego rejestrowania diagnostycznego zamiast rejestrowania w jednym scentralizowanym miejscu (programy obsługi wyjątków na poziomie aplikacji i wątku).
  • Kody powrotne mają tendencję do kierowania niechlujnym kodem w postaci zagnieżdżonych bloków „if”
  • Czas poświęcony przez programistę na debugowanie nieznanego problemu, który w przeciwnym razie byłby oczywistym wyjątkiem (dołkiem sukcesu), JEST drogi.
  • Gdyby zespół odpowiedzialny za C # nie zamierzał stosować wyjątków do zarządzania przepływem sterowania, wykonania nie byłyby wpisywane, nie byłoby filtru „when” w instrukcjach catch i nie byłoby potrzeby stosowania instrukcji „throw” bez parametrów .

Jeśli chodzi o wydajność:

  • Wyjątki mogą być kosztowne obliczeniowo Względem braku rzucania w ogóle, ale nie bez powodu są one nazywane WYJĄTKAMI. Porównania prędkości zawsze pozwalają przyjąć 100% współczynnik wyjątków, co nigdy nie powinno mieć miejsca. Nawet jeśli wyjątek jest 100-krotnie wolniejszy, jak bardzo to ma znaczenie, jeśli zdarza się tylko w 1% przypadków?
  • O ile nie mówimy o arytmetyce zmiennoprzecinkowej dla aplikacji graficznych lub czymś podobnym, cykle procesora są tanie w porównaniu z czasem programisty.
  • Koszt z perspektywy czasu niesie ten sam argument. W stosunku do zapytań do bazy danych, wywołań usług internetowych lub ładowania plików, normalny czas aplikacji będzie krótszy niż czas wyjątku. Wyjątki były prawie poniżej MICROsekundy w 2006 roku
    • Ośmielam się każdego, kto pracuje w .net, aby ustawić debuger tak, aby przerywał wszystkie wyjątki i wyłączał tylko mój kod i sprawdzał, ile wyjątków już się dzieje, o których nawet nie wiesz.
b_levitt
źródło
4

Ś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”.

Kowalstwo
źródło
4
Źle to interpretujesz. Chodziło im o to, że „jeśli twój program rzuca wyjątki w swoich normalnych przepływach, to źle”. Innymi słowy „używaj wyjątków tylko dla wyjątkowych rzeczy”.
Paweł Hajdan
4

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 .

Tomasz
źródło
Link do wpisu na blogu dotyczy raczej spojrzenia przed skokiem (sprawdzanie), a nie łatwiejszego proszenia o przebaczenie niż o pozwolenie (wyjątki lub kody powrotu). Odpowiedziałem tam swoimi przemyśleniami na ten temat (wskazówka: TOCTTOU). Ale to pytanie dotyczy innej kwestii, a mianowicie na jakich warunkach należy używać mechanizmu wyjątków języka zamiast zwracać wartość o specjalnych właściwościach.
Damian Yerrick
Całkowicie się zgadzam. Wygląda na to, że nauczyłem się jednej lub dwóch rzeczy w ciągu ostatnich dziewięciu lat;)
Thomas
4

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 :)

Gishu
źródło
3

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.

Kyle Cronin
źródło
2
Wyjątki są niezwykle kosztowne. Muszą przejść stos, aby znaleźć potencjalne programy obsługi wyjątków. Ten spacer nie jest tani. Jeśli budowany jest ślad stosu, jest jeszcze droższy, ponieważ wtedy cały stos musi zostać przeanalizowany.
Derek Park,
Dziwię się, że kompilatory nie mogą określić, gdzie wyjątek zostanie przechwycony przynajmniej przez pewien czas. Ponadto fakt, że wyjątek zmienia przepływ kodu, ułatwia moim zdaniem dokładną identyfikację miejsca wystąpienia błędu, nadrabia spadek wydajności.
Kyle Cronin,
Stosy wywołań mogą stać się niezwykle złożone w czasie wykonywania, a kompilatory generalnie nie wykonują tego rodzaju analizy. Nawet gdyby tak było, nadal musiałbyś przejść stos, aby znaleźć ślad. Nadal będziesz musiał rozwinąć stos, aby poradzić sobie z finallyblokami i destruktorami obiektów przydzielonych na stosie.
Derek Park,
Zgadzam się jednak, że korzyści związane z debugowaniem wynikające z wyjątków często rekompensują koszty wydajności.
Derek Park,
1
Derak Park, wyjątek jest drogi, kiedy się zdarzy. To jest powód, dla którego nie należy ich nadużywać. Ale kiedy się nie zdarzają, praktycznie nic nie kosztują.
paercebal
3

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ą.

Kendall Helmstetter Gelner
źródło
3

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.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

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.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

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.

Jerub
źródło
Drugi przykład jest mylący. Podobno zły sposób jest zły, ponieważ kod os.path.rmdir jest zaprojektowany do zgłaszania wyjątku. Prawidłowa implementacja w przepływie kodu powrotu to „if rmdir (...) == FAILED: pass”
MaR
3

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.

rpattabi
źródło
1
COM wad zaprojektowany do użytku w językach, które nie obsługują wyjątków.
Kevin,
To dobra uwaga. Warto zajmować się kodami błędów dla języków skryptowych. Przynajmniej VB6 dobrze ukrywa szczegóły kodu błędu, zamykając je w obiekcie Err, co w pewnym stopniu pomaga w czystszym kodzie.
rpattabi
Nie zgadzam się: VB6 rejestruje tylko ostatni błąd. W połączeniu z niesławnym „Wznów dalej po błędzie”, całkowicie przegapisz źródło problemu, zanim go zobaczysz. Zauważ, że jest to podstawa obsługi błędów w Win32 APII (patrz funkcja GetLastError)
paercebal
2

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.

Trent
źródło
2

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:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

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.

Daniel Bruce
źródło
Przynajmniej w C / C ++ i gcc możesz nadać funkcji atrybut, który będzie generował ostrzeżenie, gdy jej wartość zwracana zostanie zignorowana.
Paweł Hajdan
phjr: Chociaż nie zgadzam się ze wzorcem „kodu błędu powrotu”, Twój komentarz powinien być pełną odpowiedzią. Uważam to za wystarczająco interesujące. Przynajmniej dostarczyło mi użytecznych informacji.
paercebal
2

Istnieje wiele powodów, dla których warto preferować Wyjątki od kodu powrotu:

  • Zwykle dla czytelności ludzie starają się zminimalizować liczbę instrukcji zwracających w metodzie. W ten sposób wyjątki uniemożliwiają wykonanie dodatkowej pracy w stanie nieaktualnym, a tym samym zapobiegają potencjalnemu uszkodzeniu większej ilości danych.
  • Wyjątki są ogólnie bardziej szczegółowe i łatwiejsze do rozszerzenia niż wartość zwracana. Załóżmy, że metoda zwraca liczbę naturalną i używasz liczb ujemnych jako kodu powrotu, gdy wystąpi błąd.Jeśli zakres twojej metody zmieni się i teraz zwrócisz liczby całkowite, będziesz musiał zmodyfikować wszystkie wywołania metody zamiast tylko trochę poprawiać wyjątek.
  • Wyjątki pozwalają łatwiej oddzielić obsługę błędów od normalnego zachowania. Pozwalają one zapewnić, że niektóre operacje działają w jakiś sposób jako operacja atomowa.
gadżet
źródło
2

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:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Tak długo, jak pozwolisz wyjątkowi nadal bąbelkować, wszystko jest w porządku. Inny przykład to:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

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:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

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:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Lub, jeśli obiekt db nie implementuje IDisposable, zrób to:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

To i tak moje 2 centy! :)

noocyte
źródło
To będzie dziwne, jeśli obiekt ma .Close (), ale nie ma .Dispose ()
abatishchev
Używam tylko Close () jako przykładu. Możesz myśleć o tym jako o czymś innym. Jak stwierdzam; należy użyć wzorca using (doh!), jeśli jest dostępny. To oczywiście oznacza, że ​​klasa implementuje IDisposable i jako taka można wywołać Dispose.
noocyte
1

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.

SCdF
źródło
1

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

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

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.

Robert Gould
źródło
Do tego służy instrukcja <code> Wreszcie </code>. Ale, niestety, nie jest w standardzie C ++ ...
Thomas
W C ++ należy trzymać się praktycznej zasady „Pozyskiwanie zasobów w konstruktorze i zwalnianie ich w destruktorze. W tym konkretnym przypadku auto_ptr się nada idealnie.
Serge
Thomas, mylisz się. C ++ ostatecznie tego nie zrobił, ponieważ tego nie potrzebuje. Zamiast tego ma RAII. Rozwiązanie Serge to jedno rozwiązanie wykorzystujące RAII.
paercebal
Robert, użyj rozwiązania Serge'a, a Twój problem zniknie. Teraz, jeśli napiszesz więcej try / catch niż rzutów, to (oceniając swój komentarz) być może masz problem w swoim kodzie. Oczywiście użycie catch (...) bez ponownego rzutu jest zwykle złe, ponieważ ukrywa błąd, aby lepiej go zignorować.
paercebal
1

Moja ogólna reguła w argumencie wyjątek a kod powrotu:

  • Użyj kodów błędów, gdy potrzebujesz lokalizacji / internacjonalizacji - w .NET możesz użyć tych kodów błędów do odniesienia do pliku zasobów, który następnie wyświetli błąd w odpowiednim języku. W przeciwnym razie użyj wyjątków
  • Używaj wyjątków tylko w przypadku błędów, które są naprawdę wyjątkowe. Jeśli jest to coś, co zdarza się dość często, użyj kodu błędu logicznego lub wyliczeniowego.
Jon Limjap
źródło
Nie ma powodu, abyś nie mógł użyć wyjątku, gdy wykonujesz l10n / i18n. Wyjątki mogą również zawierać zlokalizowane informacje.
gizmo
1

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.

Jonathan Adelson
źródło
0

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.

2ank3th
źródło
0

Generalnie wolę kody zwrotne, ponieważ pozwalają dzwoniącemu zdecydować, czy awaria jest wyjątkowa .

Takie podejście jest typowe dla języka Elixir.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

Ludzie wspominali, że kody powrotne mogą spowodować, że będziesz mieć wiele zagnieżdżonych ifinstrukcji, ale można to obsłużyć dzięki lepszej składni. W Elixirze withinstrukcja ta pozwala nam łatwo oddzielić serię wartości zwracanych przez happy-path od wszelkich awarii.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

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.

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

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łączenie File.write. Albo mogę zadzwonić File.writei zająć się każdą z możliwych przyczyn niepowodzenia w inny sposób.

Oczywiście zawsze można zrobić rescuewyją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.

Nathan Long
źródło