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

Jan
źródło
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
Andrew Marshall
źródło
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.

Michael Slade
źródło
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.

Ben Aubin
źródło
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.

Sergio Tulentsev
źródło
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.

Russell Borogove
źródło
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.

calebkm
źródło