Ключевое слово retry применяется в двух случаях: в контексте итератора и в контексте блока begin-end (обработка исключений). В теле итератора (или цикла for) оно заставляет итератор заново выполнить инициализацию, то есть повторно вычислить переданные ему аргументы. Отметим, что к циклам общего вида это не относится.
Ключевое слово redo — обобщение retry на циклы общего вида. Оно работает в циклах while и until, как retry в итераторах.
Ключевое слово next осуществляет переход на конец самого внутреннего цикла и возобновляет исполнение с этой точки. Работает для любого цикла и итератора.
Как мы только что видели, итератор — важное понятие в Ruby. Но следует отметить, что язык позволяет определять и пользовательские итераторы, не ограничиваясь встроенными.
Стандартный итератор для любого объекта называется each. Это существенно отчасти из-за того, что позволяет использовать цикл for. Но итераторам можно давать и другие имена и применять для разных целей.
В качестве примера рассмотрим многоцелевой итератор, который имитирует цикл с проверкой условия в конце (как в конструкции do-while в С или repeat-until в Pascal):
def repeat(condition)
yield
retry if not condition
end
В этом примере ключевое слово yield служит для вызова блока, который задается при таком вызове итератора:
j=0
repeat (j >= 10) do
j += 1
puts j
end
С помощью yield можно также передать параметры, которые будут подставлены в список параметров блока (между вертикальными черточками). В следующем искусственном примере итератор всего лишь генерирует целые числа от 1 до 10, а вызов итератора порождает кубические степени этих чисел:
def my_sequence
for i in 1..10 do
yield i
end
end
my_sequence {|x| puts x**3 }
Отметим, что вместо фигурных скобок, в которые заключен блок, можно написать ключевые слова do и end. Различия между этими формами есть, но довольно тонкие.
1.2.7. Исключения
Как и многие другие современные языки, Ruby поддерживает исключения.
Предложение raise возбуждает исключение. Отметим, что raise — не зарезервированное слово, а метод модуля Kernel. (У него есть синоним fail.)
raise # Пример 1
raise "Произошла ошибка" # Пример 2
raise ArgumentError # Пример 3
raise ArgumentError, "Неверные данные" # Пример 4
raise ArgumentError.new("Неверные данные ") # Пример 5
raise ArgumentError, " Неверные данные ", caller[0] # Пример 6
В первом примере повторно возбуждается последнее встретившееся исключение. В примере 2 создается исключение RuntimeError (подразумеваемый тип), которому передается сообщение "Произошла ошибка".
В примере 3 возбуждается исключение типа ArgumentError, а в примере 4 такое же исключение, но с сообщением "Неверные данные". Пример 5 — просто другая запись примера 4. Наконец, в примере 6 еще добавляется трассировочная информация вида "filename:line" или "filename:line:in 'method'" (хранящаяся в массиве caller).
А как обрабатываются исключения в Ruby? Для этой цели служит блок begin-end. В простейшей форме внутри него нет ничего, кроме кода:
begin
#Ничего полезного.
#...
end
Просто перехватывать ошибки не очень осмысленно. Но у блока может быть один или несколько обработчиков rescue. Если произойдет ошибка в любой точке программы между begin и rescue, то управление сразу будет передано в подходящий обработчик rescue.
begin
x = Math.sqrt(y/z)
# ...
rescue ArgumentError
puts "Ошибка при извлечении квадратного корня."
rescue ZeroDivisionError
puts "Попытка деления на нуль."
end
Того же эффекта можно достичь следующим образом:
begin
x = Math.sqrt(y/z)
# ...
rescue => err
puts err
end
Здесь в переменной err хранится объект-исключение; при выводе ее на печать объект будет преобразован в осмысленную символьную строку. Отметим, что коль скоро тип ошибки не указан, то этот обработчик rescue будет перехватывать все исключения, производные от класса StandardError. В конструкции rescue => variable можно перед символом => дополнительно указать тип ошибки.
Если типы ошибок указаны, то может случиться так, что тип реально возникшего исключения не совпадает ни с одним из них. На этот случай после всех обработчиков rescue разрешается поместить ветвь else.
begin
# Код, в котором может возникнуть ошибка...
rescue Type1
# ...
rescue Type2
# ...
else
#Прочие исключения...
end
Часто мы хотим каким-то образом восстановиться после ошибки. В этом поможет ключевое слово retry (внутри тела обработчика rescue). Оно позволяет повторно войти в блок begin и попытаться еще раз выполнить операцию:
begin
# Код, в котором может возникнуть ошибка...
rescue
# Пытаемся восстановиться...
retry # Попробуем еще раз.
end
Наконец, иногда необходим код, который «подчищает» что-то после выполнения блока begin-end. В этом случае можно добавить часть ensure:
begin
# Код, в котором может возникнуть ошибка...
rescue
# Обработка исключений.
ensure
# Этот код выполняется в любом случае.
end
Код, помещенный внутрь части ensure, выполняется при любом способе выхода из блока begin-end — вне зависимости от того, произошло исключение или нет.
Исключения можно перехватывать еще двумя способами. Во-первых, существует форма rescue в виде модификатора:
x = a/b rescue puts("Деление на нуль!")
Кроме того, тело определения метода представляет собой неявный блок begin-end; слово begin опущено, а все тело метода подготовлено к обработке исключения и завершается словом end:
def some_method
# Код...
rescue
# Восстановление после ошибки...
end
На этом мы завершаем как обсуждение обработки исключений, так и рассмотрение основ синтаксиса и семантики в целом.
У Ruby есть многочисленные аспекты, которых мы не коснулись. Оставшаяся часть главы посвящена более развитым возможностям языка, в том числе рассмотрению ряда практических приемов, которые помогут программисту среднего уровня научиться «думать на Ruby».
1.3. ООП в Ruby
В языке Ruby есть все элементы, которые принято ассоциировать с объектно-ориентированными языками: объекты с инкапсуляцией и сокрытием данных, методы с полиморфизмом и переопределением, классы с иерархией и наследованием. Но Ruby идет дальше, добавляя ограниченные возможности создания метаклассов, синглетные методы, модули и классы-примеси.
Похожие идеи, только под иными именами, встречаются и в других объектно-ориентированных языках, но одно и то же название может скрывать тонкие различия. В этом разделе мы уточним, что в Ruby понимается под каждым из элементов ООП.
1.3.1. Объекты
В Ruby все числа, строки, массивы, регулярные выражения и многие другие сущности фактически являются объектами. Работа программы состоит в вызове методов разных объектов: