Статьи

Ruby Обработка ошибок, помимо основ

Узнайте больше о ruby ​​с нашим учебником Настройка автоматического тестирования с помощью RSpec на SitePoint.

Оповещение - точно созданные значки линий

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

Правда в том, что это не их вина. Большая часть материала на эту тему является очень простой, охватывающей простые вещи, такие как сообщение об ошибке, ее устранение, различные типы ошибок и… вот и все. Эта статья будет пытаться пойти глубже, чем это. Я предполагаю, что вы знакомы с основами обработки ошибок (с помощью метода повышение, начало / спасение, что такое StandardError, наследование ошибок). Это единственное условие для чтения этой статьи. Давайте начнем.

Что мы делали до создания исключений?

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

  • raise
  • rescue
  • begin/end (многие другие языки используют разные формулировки, такие как try/catch или throw , но идея, стоящая за этим, остается прежней.)

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

Очистка перед сбоем

Часто мы не знаем, когда произойдет сбой нашей программы. Что если нам необходимо выполнить некоторые операции очистки до завершения нашей программы (из-за ошибки)? Вот где на помощь приходит at_exit :

 puts 'Hello' at_exit do puts 'Exiting' end raise 'error' 

Эта программа напечатает «Hello» и «Exiting». Код внутри at_exit будет выполняться при выходе из программы (будь то нормально или с исключением). Если вы хотите, чтобы он запускался только при возникновении исключения, используйте глобальную переменную $! в рубине. Значение по умолчанию $! nil , он установлен для ссылки на объект исключения при возникновении ошибки:

 puts 'Hello' at_exit do if $! # If the program exits due to an exception puts 'Exiting' end end raise 'error' # Try running this code with this line and then remove it. See when "Exiting" will print. 

Люди используют at_exit для всех видов вещей, таких как создание собственного регистратора ошибок (печать сообщения в $! файл), сообщение другому приложению о том, что запущенное приложение больше не работает, и так далее.

Вы также можете передать объект Proc в at_exit вместо блока, как и в любом другом методе Ruby, который принимает блоки:

 puts 'Hello' to_execute = Proc.new { puts 'Exiting' } at_exit(&to_execute) # & converts the Proc object to a block 

Выход с треском

Давайте предположим, что вы реализовали at_exit в своей программе. Однако есть случаи, когда вы не хотите, чтобы блок внутри него выполнялся. Что вы должны сделать в этом случае? Вы должны удалить / закомментировать все внутри at_exit и снова запустить свою программу?

Есть лучший способ добавлять и удалять код все время. В Ruby есть метод с именем exit! которая является «опасной версией» exit . Основная «опасность» заключается в том, что когда вы это называете, происходят две вещи, которые обычно не происходят при использовании обычной версии:

  • Программа выйдет без выполнения блока кода внутри at_exit .
  • Код выхода устанавливается в 1 вместо 0, как в обычной (не взрываемой) версии.

Простая регистрация ошибок в любом месте вашего кода

Я поделюсь своим любимым способом реализации собственного регистратора исключений (если вам интересны другие способы сделать это, я настоятельно рекомендую Exceptional Ruby от Avdi Grimm, наиболее полный ресурс по этой теме.) Этот подход можно использовать для все виды целей, как только вы поймете основную идею.

Мы можем использовать локальную переменную потока в Ruby для хранения наших сообщений об ошибках:

 (Thread.current[:errors] ||= []) << 'Any error message goes here' 

Thread.current[:errors] как переменная доступна в любом месте вашего кода (обычно я использую переменные с глобальной областью действия, за редкими исключениями, подобными этой. Думайте об этой переменной как о очень простом глобальном обработчике ошибок.) Технически, поток -local переменные имеют область видимости потока, которая должна быть бессмысленной, если вы не пишете многопоточный код. Если вы это сделаете, знайте, что имя переменной говорит само за себя; это локально для потока (thread = ваша программа, если вы не работаете с несколькими потоками).

Вы можете использовать вышеприведенное выражение в любом месте вашего кода, из любого метода.

Чтобы избежать повторения кода, я Thread.current выражение Thread.current в отдельный метод (убедитесь, что метод доступен из любой точки вашего кода):

 def report_error(error_message) (Thread.current[:errors] ||= []) << "#{error_message}" end 

Допустим, у нас есть some_random_method который вылетает, и мы хотим some_random_method детали:

 def some_random_method begin raise 'an error!' rescue => error report_error("#{error.class} and #{error.message}") end end some_random_method p Thread.current[:errors] #=> ["RuntimeError and an error!"] 

На данный момент ошибки хранятся в локальных переменных потока. Мы хотим сохранить их в файле. Давайте напишем метод log_errors , который сделает это:

 def log_errors File.open('errors.txt', 'a') do |file| (Thread.current[:errors] ||= []).each do |error| file.puts error end end end 

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

 at_exit { log_errors } 

Будьте осторожны с вашим спасательным кодом

Трудный путь, который я усвоил, заключается в том, что вы никогда не будете слишком осторожны с кодом, который вы отправили в rescue . Что делать, если код внутри него дает сбой и создает исключение? Конечно, вы можете вкладывать средства rescue , но это противоречит всей цели заявления!

Представьте, была ли ваша программа ответственна, скажем, за строительство здания. Затем происходит пожар. В ответ на это на помощь приходит пожарный:

 begin # building the building rescue Fire Fireman.dispatch end 

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

Никогда не спасайте Exception , никогда не спасайте широко

Прочитав 2 или 3 статьи об основах обработки исключений в Ruby, вы обязательно увидите совет никогда не спасать Exception . Exception является корнем библиотеки классов исключений, «матерью всех исключений». Я хочу пойти еще дальше с этим советом и рекомендовать вам никогда не спасать в целом. Это включает в себя игнорирование широких классов, таких как StandardError (сам StandardError имеет более 300 дочерних классов, происходящих от него). Таким образом, спасая StandardError , вы обрабатываете 300 потенциальных сбоев. Теперь скажите мне, если у вас есть спасательный блок, обрабатывающий 300 возможных случаев отказа, какова вероятность того, что rescue блок сам выйдет из строя? Представьте себе, что вы предоставляете пожарному такое же оборудование для работы с одноэтажными домами и 100-этажным зданием! Не хорошая идея.

Итак, каково решение? Вместо того, чтобы разбираться в общих чертах, попытайтесь спасти определенные ошибки (которые не имеют исключений более 100 детей). Что приводит меня к моей следующей точке …

Типы исключений (по вероятности случившегося)

Многие люди проповедуют такие вещи, как «исключения должны быть исключительными», не понимая, что слово «исключительные» часто неправильно понимают. Я предлагаю лучший способ классификации конкретного исключения по вероятности того, что оно действительно произойдет:

  • Возможно случиться. Это то, что многие люди имеют в виду, когда говорят, что исключение должно быть «исключительным». Исключение, которое может произойти в ситуации, далекой от того, что вы можете ожидать.
  • Вероятно, случится . Вы можете с достаточной точностью предсказать, что в некоторых ситуациях это исключение в конечном итоге произойдет.
  • Должно (обязательно) произойти Исключение произойдет, и вы можете легко создать ситуацию для этого.

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

Если исключение возможно, но оно маловероятно, нужно ли вам его обрабатывать? Вы должны предвидеть и различать каждый возможный случай? Это главная причина, почему так много людей спасают StandardError или Exception . Они не хотят, чтобы их программа провалилась ни при каких обстоятельствах. По моему опыту (и опыту многих других людей, с которыми я разговаривал), это создает больше проблем, чем решает. Моя рекомендация — достаточно часто запускать вашу программу и смотреть, где она выходит из строя. Посмотрите, какие исключения возникли, и когда они произойдут, скажем, более двух раз, разберитесь с ними.

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

Мой совет — не спасать исключения, для которых вы понятия не имеете, произойдут ли они. Если они произошли один или два раза, и вы запускали свою программу в течение нескольких месяцев, выясните основную причину этого. Спрашивайте «почему» до тех пор, пока не получите ответ, который позволит вам исправить логику программы / системы и предотвратить повторение этой ошибки.

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

Альтернативы повышению исключений

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

Индивидуальная стратегия

Вы можете указать Ruby использовать собственную стратегию для спасения исключения (по умолчанию будет повышение). Предположим, у вас есть этот код (в случае, если вы не понимаете, где находится предложение begin , каждое определение метода является неявным оператором begin/end , то есть само def является begin ):

 def some_method(some_argument, error_strategy = method(:raise)) raise "#{some_argument}" rescue error_strategy.call end some_method(1) # '1' (RuntimeError) 

Этот код вызовет RuntimeError с сообщением в качестве нашего аргумента, RuntimeError 1 . Теперь, оставив определение метода без изменений, попробуйте следующее:

 error_handler = Proc.new { puts 'I rescued it!' } some_method(1, error_handler) 

Теперь программа завершится без ошибок, и «Я спас ее!» Выведет на консоль. Мы просто изменили нашу «стратегию» по умолчанию для обработки ошибок в методе, передавая proc объекту. Если method(:raise) часть вам незнакома, см. Эту статью .

Используйте значение, которое позволит программе продолжить

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

 def method_that_should_return_an_array this_method_call_will_raise_an_error('some argument') rescue [] # your method will return this end 

Поднять nil

Я думаю, что возвращать nil вместо возбуждения исключения — это часто используемая стратегия в сообществе Ruby. Поднимите руку, если вы получили ошибку «не могу вызвать метод X в NilClass» и были разочарованы, когда обнаружили, что часть вашего кода вернула неожиданный nil , из-за которого весь ад вышел из строя. Чтобы бороться с этой проблемой, многие люди будут делать что-то вроде этого:

 some_method_that_might_return_nil || raise 'nil returned' 

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

Когда вы возвращаете nil , хорошей идеей будет убедиться, что код позже проверит и обработает его (иначе вы получите кучу неожиданных ошибок «не могу вызвать метод X в NilClass»):

 my_value = some_method_that_might_raise_nil if my_value.nil? # do something else # do something else end 

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

Узнайте больше о ruby ​​с нашим учебником Настройка автоматического тестирования с помощью RSpec на SitePoint.