Почему плохой стиль `спасать Exception => e` в Ruby?

Руби QuickRef Райана Дэвиса говорит (без объяснения причин):

Не спасайте Исключение. КОГДА-ЛИБО. или я тебя зарежу.

Почему бы и нет? Что правильно сделать?

5 ответов

Решение

TL; DR: использовать StandardError вместо этого для общего исключения ловли. Когда исходное исключение повторно вызывается (например, при спасении для регистрации только исключения), спасение Exception наверное нормально


Exception является корнем иерархии исключений Ruby, поэтому, когда вы rescue Exception Вы спасаетесь от всего, включая подклассы, такие как SyntaxError, LoadError, а также Interrupt,

спасающий Interrupt запрещает пользователю использовать CTRLC для выхода из программы.

спасающий SignalException не позволяет программе правильно реагировать на сигналы. Это будет неубиваемым, кроме как kill -9,

спасающий SyntaxError Значит это evalТе, кто потерпит неудачу, будут делать это молча.

Все это можно показать, запустив эту программу и попытавшись выполнить CTRLC или kill Это:

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

Спасая от Exception даже не по умолчанию. дела

begin
  # iceberg!
rescue
  # lifeboats
end

не спасает от Exceptionспасает от StandardError, Как правило, вы должны указать что-то более конкретное, чем значение по умолчанию StandardError, но спасая от Exception скореерасширяет сферу, чем сужает ее, и может привести к катастрофическим результатам и сделать поиск ошибок чрезвычайно трудным.


Если у вас есть ситуация, когда вы хотите спасти от StandardError и вам нужна переменная с исключением, вы можете использовать эту форму:

begin
  # iceberg!
rescue => e
  # lifeboats
end

что эквивалентно:

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

Один из немногих распространенных случаев, когда спасение от здравого смысла Exception предназначен для регистрации / отчетности, и в этом случае вы должны немедленно повторно вызвать исключение:

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

Настоящее правило таково: не выбрасывайте исключения. Объективность автора вашей цитаты сомнительна, о чем свидетельствует тот факт, что она заканчивается

или я тебя зарежу

Конечно, имейте в виду, что сигналы (по умолчанию) генерируют исключения, и обычно длительные процессы завершаются с помощью сигнала, поэтому перехват исключений и не прекращение исключений сигналов очень затруднит остановку вашей программы. Так что не делай этого:

#! /usr/bin/ruby

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

Нет, правда, не делай этого. Даже не запускайте это, чтобы увидеть, работает ли оно.

Однако, скажем, у вас есть многопоточный сервер, и вы хотите, чтобы все исключения не:

  1. игнорироваться (по умолчанию)
  2. остановить сервер (что произойдет, если вы скажете thread.abort_on_exception = true).

Тогда это вполне приемлемо в вашей ветке обработки соединений:

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

Выше описан вариант обработчика исключений по умолчанию в Ruby с тем преимуществом, что он также не убивает вашу программу. Rails делает это в своем обработчике запросов.

Исключения сигналов возникают в основном потоке. Фоновые потоки не получат их, поэтому нет смысла пытаться их там поймать.

Это особенно полезно в производственной среде, где вы не хотите, чтобы ваша программа просто остановилась, когда что-то пойдет не так. Затем вы можете взять дампы стека в своих журналах и добавить к своему коду, чтобы обрабатывать конкретные исключения дальше по цепочке вызовов и более изящно.

Также обратите внимание, что существует другая идиома Ruby, которая имеет почти такой же эффект:

a = do_something rescue "something else"

В этой строке, если do_something возбуждает исключение, его ловит Руби, выбрасывает и a назначен "something else",

Как правило, не делайте этого, за исключением особых случаев, когда вы знаете, что вам не нужно беспокоиться. Один пример:

debugger rescue nil

debugger Функция - довольно хороший способ установить точку останова в вашем коде, но если она работает вне отладчика и Rails, она вызывает исключение. Теперь, теоретически, вы не должны оставлять отладочный код в своей программе (pff! Никто не делает этого!), Но вы можете захотеть оставить его там некоторое время по какой-то причине, но не постоянно запускать отладчик.

Замечания:

  1. Если вы запустили чужую программу, которая ловит исключения сигналов и игнорирует их (скажем, код выше), то:

    • в линуксе, в оболочке наберите pgrep ruby, или же ps | grep rubyнайдите PID вашей программы-нарушителя и запустите kill -9 <PID>,
    • в Windows используйте диспетчер задач (CTRL-SHIFT-ESC), перейдите на вкладку "процессы", найдите свой процесс, щелкните его правой кнопкой мыши и выберите "Завершить процесс".
  2. Если вы работаете с чьей-либо программой, которая по какой-либо причине усеяна этими блоками игнорирования-исключения, то размещение этого в верхней части основной строки является одним из возможных отказов:

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

    Это приводит к тому, что программа реагирует на обычные сигналы завершения, немедленно завершая работу, обходя обработчики исключений, без очистки. Так что это может привести к потере данных или тому подобное. Быть осторожен!

  3. Если вам нужно сделать это:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end
    

    вы действительно можете сделать это:

    begin
      do_something
    ensure
      critical_cleanup
    end
    

    Во втором случае critical cleanup будет вызываться каждый раз, независимо от того, выдано ли исключение.

Допустим, вы находитесь в машине (работает Руби). Вы недавно установили новое рулевое колесо с беспроводной системой обновления (которая использует eval), но вы не знали, что один из программистов испортил синтаксис.

Вы находитесь на мосту и понимаете, что идете немного к перилам, поэтому вы поворачиваете налево.

def turn_left
  self.turn left:
end

упс! Это, вероятно, не хорошо™, к счастью, Руби поднимает SyntaxError,

Машина должна немедленно остановиться - верно?

Нету.

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

звуковой сигнал звуковой сигнал

Предупреждение: поймано исключение SyntaxError.

Информация: Зарегистрированная ошибка - продолжение процесса.

Вы замечаете, что что-то не так, и хлопаете на аварийных перерывах (^C: Interrupt)

звуковой сигнал звуковой сигнал

Предупреждение: исключение прерывания прерывания.

Информация: Зарегистрированная ошибка - продолжение процесса.

Да - это не очень помогло. Ты довольно близко к рельсам, поэтому ставишь машину в парк (killING: SignalException).

звуковой сигнал звуковой сигнал

Предупреждение: поймано исключение SignalException.

Информация: Зарегистрированная ошибка - продолжение процесса.

В последнюю секунду вы вытащите ключи (kill -9), и машина останавливается, вы врезаетесь в руль (подушка безопасности не может надуваться, потому что вы не грациозно остановили программу - вы прекратили ее), и компьютер в задней части вашего автомобиля врезается в сиденье в перед этим. Наполовину полная банка кока-колы проливает на бумаги. Продукты в задней части измельчены, и большинство из них покрыты яичным желтком и молоком. Автомобиль нуждается в серьезном ремонте и чистке. (Потери данных)

Надеюсь, у вас есть страховка (резервные копии). Ах да - потому что подушка безопасности не надулась, вы, вероятно, ранены (уволены и т. Д.).


Но ждать! Там в Больше причины, по которым вы можете использовать rescue Exception => e!

Допустим, вы тот автомобиль, и вы хотите убедиться, что подушка безопасности надувается, если автомобиль превышает безопасный момент остановки.

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

Вот исключение из правила: вы можете поймать Exception только если вы повторно поднять исключение. Итак, лучшее правило - никогда не глотать Exceptionи всегда повторно поднимать ошибку.

Но добавление аварийного восстановления легко забыть на языке, подобном Ruby, и создание правильного выражения перед повторным поднятием проблемы кажется немного СУХИМЫМ. И вы не хотите забывать raise заявление. И если вы это сделаете, удачи в попытке найти эту ошибку.

К счастью, Ruby потрясающий, вы можете просто использовать ensure ключевое слово, которое обеспечивает выполнение кода. ensure Ключевое слово будет запускать код независимо от того, что - если выдается исключение, если нет, единственным исключением является то, что конец света (или другие маловероятные события).

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

Boom! И этот код должен работать в любом случае. Единственная причина, по которой вы должны использовать rescue Exception => e если вам нужен доступ к исключению, или если вы хотите, чтобы код выполнялся только для исключения. И не забудьте повторно поднять ошибку. Каждый раз.

Примечание: как указывал @Niall, всегда выполняйте. Это хорошо, потому что иногда ваша программа может лгать вам и не генерировать исключения, даже когда возникают проблемы. С критическими задачами, такими как надувные подушки безопасности, вы должны быть уверены, что это произойдет, несмотря ни на что. Из-за этого, проверка каждый раз, когда автомобиль останавливается, выбрасывается ли исключение, является хорошей идеей. Несмотря на то, что накачивание подушек безопасности является довольно редкой задачей в большинстве контекстов программирования, на самом деле это довольно часто встречается в большинстве задач по очистке.


TL; DR

не rescue Exception => e (а не повторно поднимать исключение) - или вы можете ехать с моста.

Потому что это охватывает все исключения. Вряд ли ваша программа сможет восстановиться после любого из них.

Вы должны обрабатывать только исключения, которые вы знаете, как восстановить. Если вы не ожидаете какого-то исключения, не обрабатывайте его, громко вылетайте (записывайте детали в журнал), затем диагностируйте журналы и исправляйте код.

Глотать исключения - это плохо, не делай этого.

Я только что прочитал об этом отличный пост на сайте https://www.honeybadger.io/:

Исключение Ruby против StandardError: в чем разница?

Почему не следует спасать Exception

Проблема со спасением Exception заключается в том, что он фактически спасает каждое исключение, наследуемое от Exception. Что.... все они!

Это проблема, потому что есть некоторые исключения, которые используются внутри Ruby. Они не имеют ничего общего с вашим приложением, и их проглатывание приведет к плохим вещам.

Вот несколько из них:

  • SignalException::Interrupt - Если вы спасете это, вы не сможете выйти из приложения, нажав Ctrl-c.

  • ScriptError::SyntaxError - Проглатывание синтаксических ошибок означает, что такие вещи, как put("Что-то забыл), не будут работать без предупреждения.

  • NoMemoryError - Хотите знать, что происходит, когда ваша программа продолжает работать после того, как израсходует всю оперативную память? И я нет.

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

Я предполагаю, что вы действительно не хотите проглатывать ни одно из этих исключений системного уровня. Вы хотите только поймать все ошибки на уровне вашего приложения. Исключения вызвали ВАШ код.

К счастью, к этому есть простой способ.

Это особый случай правила, что вы не должны ловить исключения, которые вы не знаете, как их обработать. Если вы не знаете, как с этим справиться, всегда лучше позволить какой-то другой части системы перехватить и обработать ее.

Это также скрывает ошибки от вас, например, если вы ошиблись при вводе имени метода:

def my_fun
  "my_fun"
end

begin
 # you mistypped my_fun to my_func
 my_func # my_func()
rescue Exception
  # rescued NameError (or NoMethodError if you called method with parenthesis)
end
Другие вопросы по тегам