Чому в Ruby поганий стиль `рятувати виняток => e`?


894

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

Не рятуйте виняток. ВСЕ. або я буду колоти тебе.

Чому ні? Що правильно робити?


35
Тоді ви, мабуть, могли написати своє? :)
Серхіо Туленцев

65
Мені тут дуже незручно закликати до насильства. Це просто програмування.
Дарт Егрегійний

1
Погляньте на цю статтю у Ruby Exception із приємною ієрархією Ruby Exception .
Атул Хандурі

2
Тому що Райан Девіс буде колоти тебе. Тож діти. Ніколи не рятуйте винятків.
Муген

7
@DarthEgreious Я не можу реально сказати, жартуєте ви чи ні. Але я думаю, що це весело. (І це, очевидно, не є серйозною загрозою). Тепер кожен раз, коли я замислююся над тим, щоб зловити виняток, я вважаю, чи варто його колоти якийсь випадковий хлопець в Інтернеті.
Стів Сетер

Відповіді:


1375

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 # not enough lifeboats ;)
end

129
тож це схоже на ловлю Throwableв яві
храповий вирод

53
Ця порада корисна для чистого середовища Ruby. Але, на жаль, ряд дорогоцінних каменів створили винятки, що безпосередньо сходять із винятку. Наше середовище налічує 30 з них: напр., OpenID :: сервер :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Це винятки, які ви дуже хочете зловити в стандартних рятувальних блоках. На жаль, ніщо в Ruby не заважає або навіть відштовхує дорогоцінні камені від успадкування безпосередньо від Винятку - навіть іменування не інтуїтивно.
Джонатан Суорц

20
@JonathanSwartz Тоді порятунок із цих конкретних підкласів, а не виняток. Більш конкретний майже завжди кращий і зрозуміліший.
Ендрю Маршалл

22
@JonathanSwartz - Я б попросив творців дорогоцінних каменів змінити те, з чого наслідує їх виняток. Особисто мені подобається, що у моїх дорогоцінних каменів всі винятки походять з MyGemException, тож ви зможете врятувати це, якщо хочете.
Натан Лонг

12
Ви також можете ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]і тодіrescue *ADAPTER_ERRORS => e
j_mcnally

83

Реальне правило: Чи не викидати виключення. Об'єктивність автора вашої цитати сумнівна, про що свідчить той факт, чим вона закінчується

або я буду колоти тебе

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

#! /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 це робить у своєму оброблювачем запитів.

Винятки з сигналу піднімаються в основній нитці. Фонові нитки їх не отримають, тому немає сенсу намагатися їх зачепити там.

Це особливо корисно у виробничих умовах, коли ви не хочете, щоб ваша програма просто зупинялася, коли щось пішло не так. Тоді ви можете взяти скиди стеків у свої журнали та додати до свого коду, щоб вирішити конкретні винятки далі за ланцюжками дзвінків і більш витончено.

Зауважте також, що існує ще одна ідіома Рубі, яка має такий же ефект:

a = do_something rescue "something else"

У цьому рядку, якщо do_somethingпіднімає виняток, його ловить Рубі, викидає та aприсвоює "something else".

Як правило, не робіть цього, крім випадків, коли ви знаєте , що не потрібно хвилюватися. Один приклад:

debugger rescue nil

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

Примітка:

  1. Якщо ви запустили чужу програму, яка ловить винятки з сигналу та ігнорує їх (скажімо код вище), тоді:

    • в Linux, в оболонці, введіть 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буде викликатися кожен раз, викинуто чи не виняток.


21
Вибачте, це неправильно. Сервер ніколи не повинен врятувати виняток і нічого не робити, окрім журналу. Це зробить його нездатним за винятком випадків kill -9.
Джон

8
Ваші приклади у примітці 3 не є еквівалентними, ensureзапуск буде запущений незалежно від того, є виняток, піднятий чи ні, тоді як rescueзапуск буде виконуватися лише в тому випадку, якщо виняток був піднятий.
Ендрю Маршалл

1
Вони не / точно / еквівалент, але я не можу зрозуміти, як коротко виразити еквівалентність способом, який не є некрасивим.
Майкл Слейд

3
Просто додайте ще один виклик критичного_чищення після першого / рятувального блоку в першому прикладі. Я погоджуюся не з найелегантнішим кодом, але очевидно другим прикладом є елегантний спосіб зробити це, тому невелика неелегантність - це лише частина прикладу.
gtd

3
"Навіть не запускайте це, щоб побачити, чи працює він." здається поганою порадою щодо кодування ... Навпаки, я б радив вам запустити його, побачити його невдало і зрозуміти самостійно, як якщо не вдасться, а не сліпо вірити комусь іншому. Все одно чудова відповідь :)
huelbois

69

TL; DR

Чи не rescue Exception => e(і не ререйз виключення) - або ви могли б відігнати міст.


Скажімо, ви знаходитесь в машині (працює Рубі). Нещодавно ви встановили нове рульове колесо із системою оновлення до повітря (яка використовує 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

БІП біп

Попередження: Виняток спійманого синтаксису.

Інформація: помилка зафіксованого - процес, що триває.

Ви помітили що - то не так, і ви грюкнути на аварійних перерв ( ^C: Interrupt)

БІП біп

Попередження: Виняток перерви, що перервався.

Інформація: помилка зафіксованого - процес, що триває.

Так - це мало допомогло. Ви досить близько до залізниці, тому ви поставили машину в парк ( killing:) 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

Бум! І цей код повинен працювати в будь-якому випадку. Єдина причина, яку ви повинні використовувати, rescue Exception => e- це якщо вам потрібен доступ до винятку, або якщо ви хочете, щоб код працював лише за винятком. І не забудьте повторно підняти помилку. Кожного разу.

Примітка. Як зазначав @Niall, переконайтеся, що завжди працює. Це добре, тому що іноді ваша програма може брехати вам і не кидати винятків, навіть коли виникають проблеми. При вирішальних завданнях, таких як надуття подушок безпеки, вам потрібно переконатися, що це відбувається незалежно від того. Через це перевірка кожного разу, коли зупиняється машина, викинуто виняток чи ні, є хорошою ідеєю. Незважаючи на те, що надування подушок безпеки є дещо рідкісним завданням у більшості програм програмування, це насправді досить часто в більшості завдань очищення.


12
Хахахаха! Це чудова відповідь. Я вражений тим, що ніхто не коментував. Ви даєте чіткий сценарій, який робить всю справу справді зрозумілою. Ура! :-)
Джеймс Мілані

@JamesMilani Дякую!
Бен Оббін

3
+ 💯 для цієї відповіді. Бажаю, щоб я міг подати заявку не раз! 😂
engineerDave

1
Приємна ваша відповідь!
Атул Вайбхав

3
Ця відповідь з’явилася через 4 роки після цілком зрозумілої та правильної прийнятої відповіді, і повторно пояснив її абсурдним сценарієм, розробленим скоріше кумедним, ніж реалістичним. Вибачте, що є незрозумілою, але це не редакція - важливіше, щоб відповіді були лаконічними та правильними, ніж смішними. Також частина про ensureальтернативу rescue Exceptionвводить в оману - приклад передбачає, що вони є рівнозначними, але як зазначено ensure, відбудеться виняток чи ні, тож тепер ваші подушки безпеки надутимуться, тому що ви пройшли понад 5 км / год, хоча нічого не пішло не так.
Найлл

47

Тому що це фіксує всі винятки. Навряд чи ваша програма може відновитись із будь-якого з них.

Вам слід поводитися лише з винятками, з яких ви знаєте, як відновитись. Якщо ви не передбачаєте певного винятку, не обробляйте це, гучно вибивайте (запишіть деталі в журнал), тоді діагностуйте журнали та виправляйте код.

Ковтання винятків погано, не робіть цього.


10

Це специфічний випадок правила, що ви не повинні ловити жодне виняток, з яким ви не знаєте, як впоратися. Якщо ви не знаєте, як з цим впоратися, завжди краще дозволити деякій іншій частині системи вловлювати та обробляти її.


0

Я просто прочитав чудову публікацію про це на сайті honeybadger.io :

Виняток Рубі проти StandardError: в чому різниця?

Чому не слід рятувати виняток

Проблема з врятуванням винятку полягає в тому, що він фактично рятує кожен виняток, який успадковується від винятку. Що таке .... всі вони!

Це проблема, оскільки є деякі винятки, які використовує Рубі всередині. Вони не мають нічого спільного з вашим додатком, і проковтування їх спричинить погані речі.

Ось кілька великих:

  • SignalException :: Перерва - Якщо ви врятуєте це, ви не можете вийти зі свого додатка, натиснувши control-c.

  • ScriptError :: SyntaxError - Поглинання помилок синтаксису означає, що такі речі, як ставки ("Забули щось), вийдуть з ладу.

  • NoMemoryError - Хочете знати, що відбувається, коли програма продовжує працювати після того, як вона використовує всю оперативну пам’ять? Я теж ні.

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

Я здогадуюсь, що ви насправді не хочете ковтати жоден із цих винятків на рівні системи. Ви хочете лише зафіксувати всі ваші помилки на рівні програми. Винятки спричинили ВАШ код.

На щастя, існує простий шлях до цього.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.