Обработка исключений
В Crystal ошибки обрабатываются с помощью выбрасывания и перехвата исключений.
Выбрасывание исключения
Исключения выбрасываются вызовом метода верхнего уровня raise. В отличие от других ключевых слов, raise — это обычный метод с двумя перегрузками: одна принимает String, а другая принимает экземпляр Exception:
raise "OH NO!"
raise Exception.new("Some error")
Вариант со String просто создаёт новый экземпляр Exception с указанным сообщением.
Выбрасывать можно только экземпляры Exception или его подклассов.
Определение пользовательских исключений
Чтобы определить пользовательский тип исключения, достаточно создать подкласс Exception:
class MyException < Exception
end
class MyOtherException < Exception
end
Как и всегда, для исключения можно определить конструктор или использовать конструктор по умолчанию.
Перехват исключений
Чтобы перехватить любое исключение, используйте выражение begin ... rescue ... end:
begin
raise "OH NO!"
rescue
puts "Rescued!"
end
# Output: Rescued!
Чтобы получить доступ к перехваченному исключению, можно указать переменную в условии rescue:
begin
raise "OH NO!"
rescue ex
puts ex.message
end
# Output: OH NO!
Чтобы перехватить исключение только одного типа (или любого из его подклассов):
begin
raise MyException.new("OH NO!")
rescue MyException
puts "Rescued MyException"
end
# Output: Rescued MyException
А чтобы получить к нему доступ, используйте синтаксис, похожий на ограничения типов:
begin
raise MyException.new("OH NO!")
rescue ex : MyException
puts "Rescued MyException: #{ex.message}"
end
# Output: Rescued MyException: OH NO!
Можно указать несколько условий rescue:
begin
# ...
rescue ex1 : MyException
# only MyException...
rescue ex2 : MyOtherException
# only MyOtherException...
rescue
# any other kind of exception
end
Также можно перехватить сразу исключения нескольких типов, указав объединённый тип:
begin
# ...
rescue ex : MyException | MyOtherException
# only MyException or MyOtherException
rescue
# any other kind of exception
end
else
Условие else выполняется, только если ни одно исключение не было перехвачено:
begin
something_dangerous
rescue
# execute this if an exception is raised
else
# execute this if an exception isn't raised
end
Условие else можно указать, только если указано хотя бы одно условие rescue.
ensure
Условие ensure выполняется в конце выражения begin ... end или begin ... rescue ... end независимо от того, было ли выброшено исключение:
begin
something_dangerous
ensure
puts "Cleanup..."
end
# Will print "Cleanup..." after invoking something_dangerous,
# regardless of whether it raised or not
Или:
begin
something_dangerous
rescue
# ...
else
# ...
ensure
# this will always be executed
end
Условия ensure обычно используются для очистки, освобождения ресурсов и тому подобного.
Краткая форма синтаксиса
Для обработки исключений предусмотрена краткая форма синтаксиса: считайте, что определение метода или блока — это неявное выражение begin ... end, а затем укажите условия rescue, else и ensure:
def some_method
something_dangerous
rescue
# execute if an exception is raised
end
# The above is the same as:
def some_method
begin
something_dangerous
rescue
# execute if an exception is raised
end
end
С ensure:
def some_method
something_dangerous
ensure
# always execute this
end
# The above is the same as:
def some_method
begin
something_dangerous
ensure
# always execute this
end
end
# Similarly, the shorthand also works with blocks:
(1..10).each do |n|
# potentially dangerous operation
rescue
# ..
else
# ..
ensure
# ..
end
Вывод типов
Переменные, объявленные внутри части begin обработчика исключений, также получают тип Nil, если рассматривать их внутри тела rescue или ensure. Например:
begin
a = something_dangerous_that_returns_Int32
ensure
puts a + 1 # error, undefined method '+' for Nil
end
Это происходит даже в том случае, если something_dangerous_that_returns_Int32 никогда не выбрасывает исключение или если a присвоили значение, а затем выполнили метод, который потенциально может выбросить исключение:
begin
a = 1
something_dangerous
ensure
puts a + 1 # error, undefined method '+' for Nil
end
Хотя очевидно, что a всегда будет присвоено значение, компилятор всё равно считает, что a могло не получить возможность инициализироваться. Возможно, в будущем эта логика улучшится, но сейчас она заставляет сводить обработчики исключений к необходимому минимуму, благодаря чему намерение кода становится яснее:
# Clearer than the above: `a` doesn't need
# to be in the exception handling code.
a = 1
begin
something_dangerous
ensure
puts a + 1 # works
end
Альтернативные способы обработки ошибок
Хотя исключения — один из механизмов обработки ошибок, это не единственный доступный вариант. Выбрасывание исключения требует выделения памяти, а выполнение обработчика исключений обычно происходит медленно.
Стандартная библиотека обычно предоставляет пару методов для выполнения одной задачи: один выбрасывает исключение, другой возвращает nil. Например:
array = [1, 2, 3]
array[4] # raises because of IndexError
array[4]? # returns nil because of index out of bounds
Обычно также предоставляют альтернативный «вопросительный» метод, чтобы показать, что этот вариант метода возвращает nil вместо выбрасывания исключения. Это позволяет пользователю выбрать, обрабатывать ли исключения или nil. Однако такая возможность есть не у каждого метода: исключения по-прежнему предпочтительнее, поскольку они не засоряют код логикой обработки ошибок.
To the extent possible under law, the persons who contributed to this workhave waived
all copyright and related or neighboring rights to this workby associating CC0 with it.
https://crystal-lang.org/reference/1.2/syntax_and_semantics/exception_handling.html