Dlaczego złym stylem jest `uratowanie wyjątku => e` w Rubim?


894

Ryana Davisa Ruby QuickRef mówi (bez wyjaśnienia):

Nie ratuj wyjątku. ZAWSZE. albo cię dźgnę.

Dlaczego nie? Co należy zrobić?


35
Więc prawdopodobnie mógłbyś napisać własny? :)
— Sergio Tulentsev

65
Jestem bardzo nieswojo z powodu wezwania do przemocy tutaj. To tylko programowanie.
— Darth Egregious,

1
Spójrz na ten artykuł w Ruby Exception z ładną hierarchią Ruby Exception .
— Atul Khanduri,

2
Ponieważ Ryan Davis cię dźgnie. Więc dzieci. Nigdy nie ratuj wyjątków.
— Mugen,

7
@DarthEgregious Naprawdę nie mogę powiedzieć, czy żartujesz, czy nie. Ale myślę, że to przezabawne. (I oczywiście nie jest to poważne zagrożenie). Teraz za każdym razem, gdy myślę o wyłapaniu wyjątku, zastanawiam się, czy „warto zadźgać go jakiś przypadkowy facet w Internecie.
— Steve Sether

Odpowiedzi:


1375

TL; DR : Użyj StandardErrorzamiast tego do wychwytywania wyjątków ogólnych. Kiedy pierwotny wyjątek zostanie ponownie zgłoszony (np. Podczas ratowania w celu zarejestrowania wyjątku), ratowanie Exceptionjest prawdopodobnie w porządku.


Exceptionjest korzeniem hierarchii wyjątków Ruby , więc gdy rescue Exceptionratowanie od wszystkiego , w tym podklas, takich jak SyntaxError, LoadErrori Interrupt.

Uratowanie Interruptuniemożliwia użytkownikowi CTRLCzamknięcie programu.

Ratowanie SignalExceptionzapobiega prawidłowej reakcji programu na sygnały. Będzie to niemożliwe do zabicia, chyba że przez kill -9.

Ratowanie SyntaxErroroznacza, evalże porażka zrobi to po cichu.

Wszystkie one mogą być wyświetlane przez ten program działa, i stara się CTRLCbądź killto:

loop do
  begin
    sleep 1
    eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"
  rescue Exception
    puts "I refuse to fail or be stopped!"
  end
end

Ratowanie przed Exceptionnie jest nawet domyślne. Robić

begin
  # iceberg!
rescue
  # lifeboats
end

nie ratuje Exception, ratuje StandardError. Powinieneś ogólnie określić coś bardziej szczegółowego niż domyślny StandardError, ale ratowanie przed Exception rozszerzeniem zakresu zamiast zawężeniem go, może mieć katastrofalne skutki i bardzo utrudnić polowanie na błędy.


Jeśli masz sytuację, w której chcesz uratować StandardErrori potrzebujesz zmiennej z wyjątkiem, możesz użyć tego formularza:

begin
  # iceberg!
rescue => e
  # lifeboats
end

co jest równoważne z:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end

Jednym z niewielu typowych przypadków, w których warto ratować, Exceptionjest rejestrowanie / raportowanie, w którym to przypadku należy natychmiast ponownie zgłosić wyjątek:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end

129
więc to jest jak złapanie Throwablew java
— maniak zapadkowy

53
Ta rada jest dobra dla czystego środowiska Ruby. Ale niestety wiele klejnotów stworzyło wyjątki, które bezpośrednio wywodzą się z wyjątku. Nasze środowisko ma 30 takich: np. OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Są to wyjątki, które bardzo chciałbyś uchwycić w standardowych blokach ratunkowych. Niestety, nic w Ruby nie zapobiega ani nawet nie zniechęca klejnotów do dziedziczenia bezpośrednio po wyjątku - nawet nazywanie jest nieintuicyjne.
— Jonathan Swartz,

20
@JathanathanSwartz Następnie uratuj te konkretne podklasy, a nie wyjątki. Bardziej szczegółowe jest prawie zawsze lepsze i jaśniejsze.
— Andrew Marshall,

22
@JathanathanSwartz - Chciałbym wprowadzić błąd w twórcach klejnotów, aby zmienić to, co dziedziczy ich wyjątek. Osobiście podoba mi się, że moje klejnoty mają wyjątki od MyGemException, więc możesz je uratować, jeśli chcesz.
— Nathan Long

12
Możesz także, ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]a następnierescue *ADAPTER_ERRORS => e
— j_mcnally

83

Prawdziwa zasada: Nie wyrzucać wyjątki. Obiektywność autora cytatu jest wątpliwa, o czym świadczy fakt, że kończy się on

albo cię dźgnę

Oczywiście należy pamiętać, że sygnały (domyślnie) generują wyjątki, a zwykle długotrwałe procesy są kończone przez sygnał, więc wychwycenie wyjątku i nie zakończenie wyjątków sygnału sprawi, że program będzie bardzo trudny do zatrzymania. Więc nie rób tego:

#! /usr/bin/ruby

while true do
  begin
    line = STDIN.gets
    # heavy processing
  rescue Exception => e
    puts "caught exception #{e}! ohnoes!"
  end
end

Nie, naprawdę, nie rób tego. Nawet nie uruchamiaj tego, aby zobaczyć, czy to działa.

Załóżmy jednak, że masz serwer wątkowy i chcesz, aby wszystkie wyjątki nie:

  1. być ignorowanym (domyślnie)
  2. zatrzymaj serwer (co dzieje się, jeśli tak mówisz thread.abort_on_exception = true).

Jest to całkowicie dopuszczalne w wątku obsługi połączenia:

begin
  # do stuff
rescue Exception => e
  myLogger.error("uncaught #{e} exception while handling connection: #{e.message}")
    myLogger.error("Stack trace: #{backtrace.map {|l| "  #{l}\n"}.join}")
end

Powyższe działa w odmianie domyślnej procedury obsługi wyjątków Ruby, z tą zaletą, że nie zabija również twojego programu. Rails robi to w module obsługi żądań.

Wyjątki sygnałowe są zgłaszane w głównym wątku. Nici w tle ich nie dostaną, więc nie ma sensu próbować ich tam łapać.

Jest to szczególnie przydatne w środowisku produkcyjnym, w którym nie chcesz, aby Twój program po prostu zatrzymywał się, gdy coś pójdzie nie tak. Następnie możesz wykonać zrzuty stosu w dziennikach i dodać do kodu, aby zająć się określonym wyjątkiem w dalszej części łańcucha połączeń i w bardziej wdzięczny sposób.

Zauważ też, że istnieje inny idiom Ruby, który ma podobny efekt:

a = do_something rescue "something else"

W tej linii, jeśli do_somethingpodniesie wyjątek, zostaje złapany przez Ruby, wyrzucony i aprzypisany "something else".

Zasadniczo nie rób tego, z wyjątkiem szczególnych przypadków, w których wiesz , że nie musisz się martwić. Jeden przykład:

debugger rescue nil

The debugger funkcja jest raczej dobrym sposobem na ustawienie punktu przerwania w kodzie, ale jeśli działa poza debuggerem i Railsami, zgłasza wyjątek. Teraz teoretycznie nie powinieneś zostawiać kodu debugującego leżącego w twoim programie (pff! Nikt tego nie robi!), Ale możesz chcieć go tam zatrzymać z jakiegoś powodu, ale nie uruchamiać debuggera w sposób ciągły.

Uwaga:

  1. Jeśli uruchomiłeś program innej osoby, który przechwytuje wyjątki sygnałów i ignoruje je (powiedz kod powyżej), to:

    • w Linuksie, w powłoce, wpisz pgrep rubylub ps | grep rubywyszukaj PID programu, który go narusza, a następnie uruchom kill -9 <PID>.
    • w Windows użyj Menedżera zadań ( CTRL- SHIFT- ESC), przejdź do zakładki „procesy”, znajdź swój proces, kliknij go prawym przyciskiem myszy i wybierz „Zakończ proces”.
  2. Jeśli pracujesz z programem innej osoby, który z jakiegoś powodu jest obsadzony tymi blokami wyjątków ignorowania, umieszczenie tego na górze głównej linii jest jednym z możliwych sposobów:

    %W/INT QUIT TERM/.each { |sig| trap sig,"SYSTEM_DEFAULT" }

    Powoduje to, że program reaguje na normalne sygnały zakończenia, natychmiast kończąc, omijając procedury obsługi wyjątków, bez czyszczenia . Może to spowodować utratę danych lub podobne. Bądź ostrożny!

  3. Jeśli musisz to zrobić:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end

    możesz to zrobić:

    begin
      do_something
    ensure
      critical_cleanup
    end

    W drugim przypadku critical cleanupbędzie wywoływany za każdym razem, niezależnie od tego, czy zostanie zgłoszony wyjątek.


21
Przepraszam, to źle Serwer nigdy nie powinien ratować wyjątku i nic nie robić, tylko go rejestrować. To sprawi, że będzie nie do zabicia, chyba że przez kill -9.
— Jan

8
Twoje przykłady w nocie 3 nie są ekwiwalentne, ensurebędą działać niezależnie od tego, czy wystąpił wyjątek, czy nie, a rescuebędą działać tylko wtedy, gdy wyjątek został zgłoszony.
— Andrew Marshall,

1
Nie są / dokładnie / równoważne, ale nie mogę wymyślić, jak zwięźle wyrazić równoważność w sposób, który nie jest brzydki.
— Michael Slade

3
Wystarczy dodać kolejne wywołanierytyczne_krytyczne po bloku rozpoczęcia / ratowania w pierwszym przykładzie. Nie zgadzam się z najbardziej eleganckim kodem, ale oczywiście drugi przykład to elegancki sposób na zrobienie tego, więc mała nieelegancja jest tylko częścią przykładu.
— gtd

3
„Nawet nie uruchamiaj tego, aby sprawdzić, czy to działa”. wydaje się złą radą do kodowania ... Wręcz przeciwnie, radziłbym ci go uruchomić, zobaczyć, jak się nie udaje i samemu zrozumieć, jak to się dzieje, zamiast ślepo wierzyć komuś innemu. W każdym razie świetna odpowiedź :)
— huelbois,

69

TL; DR

Nie rób rescue Exception => ewyjątku (i nie podnosz go ponownie) - możesz też zjechać z mostu.


Powiedzmy, że jesteś w samochodzie (z Ruby). Niedawno zainstalowałeś nową kierownicę z bezprzewodowym systemem uaktualnień (który wykorzystuje eval), ale nie wiedziałeś, że jeden z programistów pomylił się co do składni.

Jesteś na moście i zdajesz sobie sprawę, że idziesz trochę w stronę balustrady, więc skręcasz w lewo.

def turn_left
  self.turn left:
end

ups! To pewnie Not Good ™, na szczęście Ruby podnosi SyntaxError.

Samochód powinien natychmiast się zatrzymać - prawda?

Nie.

begin
  #...
  eval self.steering_wheel
  #...
rescue Exception => e
  self.beep
  self.log "Caught #{e}.", :warn
  self.log "Logged Error - Continuing Process.", :info
end

sygnał dźwiękowy

Ostrzeżenie: Caught SyntaxError Exception.

Informacje: zarejestrowany błąd - kontynuacja procesu.

Można zauważyć, że coś jest nie tak, i zatrzasnąć na przerwach awaryjnych ( ^C: Interrupt)

sygnał dźwiękowy

Ostrzeżenie: wyjątek Caught Interrupt.

Informacje: zarejestrowany błąd - kontynuacja procesu.

Tak - to niewiele pomogło. Jesteś dość blisko szyny, więc ustawiasz samochód na parkingu ( killing:) SignalException.

sygnał dźwiękowy

Ostrzeżenie: Caught SignalException Exception.

Informacje: zarejestrowany błąd - kontynuacja procesu.

W ostatniej sekundzie wyciągasz klawisze ( kill -9), a samochód zatrzymuje się, rzucasz się do kierownicy (poduszka powietrzna nie może się napompować, ponieważ nie zatrzymałeś programu z wdziękiem - zakończyłeś go), a komputer z tyłu samochodu wbija się w siedzenie przed nim. Na papier wypełnia się do połowy pełna puszka coli. Artykuły spożywcze z tyłu są kruszone, a większość z nich jest pokryta żółtkiem jaja i mlekiem. Samochód wymaga poważnej naprawy i czyszczenia. (Utrata danych)

Mam nadzieję, że masz ubezpieczenie (kopie zapasowe). O tak - ponieważ poduszka powietrzna się nie napompowała, prawdopodobnie jesteś ranny (wyrzucenie z pracy itp.).


Ale poczekaj! Jestwięcejpowody, dla których możesz chcieć użyć rescue Exception => e!

Powiedzmy, że jesteś tym samochodem i chcesz się upewnić, że poduszka powietrzna się napompuje, jeśli samochód przekroczy swój bezpieczny moment zatrzymania.

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
    raise
 end

Oto wyjątek od reguły: Możesz złapać Exception tylko wtedy, gdy podniesiesz wyjątek . Lepszą zasadą jest, aby nigdy nie połykać Exceptioni zawsze podnosić błąd.

Ale dodanie ratunku jest łatwe do zapomnienia w języku takim jak Ruby, a umieszczenie oświadczenia ratunkowego tuż przed ponownym podniesieniem problemu wydaje się trochę pozbawione SUSZENIA. I nie chcesz zapomnieć raiseoświadczenia. A jeśli tak, powodzenia w znalezieniu tego błędu.

Na szczęście Ruby jest niesamowity, możesz po prostu użyć ensuresłowa kluczowego, co zapewnia uruchomienie kodu. Słowo ensurekluczowe uruchomi kod bez względu na wszystko - jeśli zostanie zgłoszony wyjątek, jeśli nie jest, jedynym wyjątkiem jest koniec świata (lub inne mało prawdopodobne zdarzenia).

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
 end

Bum! Ten kod i tak powinien działać. Jedynym powodem, dla którego powinieneś użyć rescue Exception => ejest to, że potrzebujesz dostępu do wyjątku lub jeśli chcesz, aby kod działał tylko na wyjątku. I pamiętaj, aby ponownie podnieść błąd. Każdego razu.

Uwaga: Jak wskazał @Niall, upewnij się, że zawsze działa. Jest to dobre, ponieważ czasami twój program może cię okłamywać i nie rzucać wyjątków, nawet gdy występują problemy. W przypadku krytycznych zadań, takich jak nadmuchiwanie poduszek powietrznych, musisz upewnić się, że tak się stanie, bez względu na wszystko. Z tego powodu dobrym pomysłem jest sprawdzanie za każdym razem, kiedy samochód się zatrzymuje, czy wyjątek jest zgłaszany. Chociaż nadmuchiwanie poduszek powietrznych jest nieco rzadkim zadaniem w większości kontekstów programowania, w rzeczywistości jest to dość powszechne w przypadku większości zadań czyszczenia.


12
Hahahaha! To świetna odpowiedź. Jestem zszokowany, że nikt nie skomentował. Dajesz jasny scenariusz, dzięki któremu wszystko jest naprawdę zrozumiałe. Twoje zdrowie! :-)
— James Milani,

@JamesMilani Dziękujemy!
— Ben Aubin

3
+ 💯 za tę odpowiedź. Chciałbym móc głosować więcej niż raz! 😂
— inżynierDave

1
Podobała Ci się Twoja odpowiedź!
— Atul Vaibhav

3
Ta odpowiedź nadeszła 4 lata po całkowicie zrozumiałej i poprawnej, zaakceptowanej odpowiedzi, i ponownie wyjaśniła ją absurdalnym scenariuszem, zaprojektowanym bardziej jako zabawny niż realistyczny. Przykro mi, że to buzzkill, ale to nie jest reddit - ważniejsze jest, aby odpowiedzi były zwięzłe i poprawne niż śmieszne. Również część, ensurektóra jest alternatywą dla, rescue Exceptionjest myląca - przykład sugeruje, że są one równoważne, ale jak stwierdzono, ensurestanie się to bez względu na to, czy istnieje wyjątek, czy nie, więc teraz twoje poduszki powietrzne się napompują, ponieważ przekroczyłeś 5 mil na godzinę, chociaż nic nie poszło źle.
— Niall

47

Ponieważ obejmuje to wszystkie wyjątki. Jest mało prawdopodobne, że Twój program może odzyskać od któregokolwiek z nich.

Powinieneś obsługiwać tylko wyjątki, z których wiesz, jak odzyskać. Jeśli nie spodziewasz się pewnego rodzaju wyjątku, nie obsługuj go, głośno upaść (zapisz szczegóły w dzienniku), a następnie zdiagnozuj dzienniki i napraw kod.

Połknięcie wyjątków jest złe, nie rób tego.


10

Jest to szczególny przypadek reguły, że nie należy wychwytywać żadnych wyjątków, z którymi nie można sobie poradzić. Jeśli nie wiesz, jak sobie z tym poradzić, zawsze lepiej jest pozwolić, aby inna część systemu go złapała i obsługiwała.


0

Właśnie przeczytałem świetny wpis na blogu na stronie honeybadger.io :

Wyjątek Ruby kontra StandardError: Jaka jest różnica?

Dlaczego nie powinieneś ratować wyjątku?

Problem z ratowaniem wyjątku polega na tym, że faktycznie ratuje on każdy wyjątek, który dziedziczy po wyjątku. Który to .... wszyscy z nich!

Jest to problem, ponieważ istnieją pewne wyjątki, które są używane wewnętrznie przez Ruby. Nie mają nic wspólnego z Twoją aplikacją, a ich połknięcie spowoduje złe rzeczy.

Oto kilka dużych:

  • SignalException :: Interrupt - Jeśli to uratujesz, nie możesz wyjść z aplikacji, naciskając Control-C.

  • ScriptError :: SyntaxError - Połknięcie błędów składniowych oznacza, że ​​rzeczy takie jak puts („Forgot something”) zawiodą po cichu.

  • NoMemoryError - Chcesz wiedzieć, co się stanie, gdy Twój program będzie działał po zużyciu całej pamięci RAM? Ja też nie.

begin
  do_something()
rescue Exception => e
  # Don't do this. This will swallow every single exception. Nothing gets past it. 
end

Zgaduję, że tak naprawdę nie chcesz połknąć żadnego z tych wyjątków na poziomie systemu. Chcesz tylko wyłapać wszystkie błędy na poziomie aplikacji. Wyjątki spowodowały TWÓJ kod.

Na szczęście jest na to prosty sposób.

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