Hash
аналогичен, но у него есть два параметра типа — типы ключей и типы значений.
Давайте посмотрим на простой пример. Предположим, вы хотите создать класс, который содержит значение в одной из переменных экземпляра, но это значение может быть любого типа. Давайте посмотрим, как мы можем это сделать:
class Holder(T)
def initialize(@value : T)
end
def get
@value
end
def set(new_value : T)
@value = new_value
end
end
Общие параметры, по соглашению, представляют собой одиночные заглавные буквы — в данном случае T
. В этом примере Holder
является универсальным классом, а Holder(Int32)
будет универсальным экземпляром этого класса: обычным классом, который может создавать объекты. Переменная экземпляра @value
имеет тип T
, независимо от того, какое T
будет позже. Вот как можно использовать этот класс:
num = Holder(Int32).new(10)
num.set 40
p num.get # Prints 40.
В этом примере мы создаем новый экземпляр класса Holder(Int32)
. Это как если бы у вас был абстрактный класс Holder
и наследуемый от него класс Holder_Int32
, созданный по требованию для T=Int32
. Объект можно использовать как любой другой. Методы вызываются и взаимодействуют с переменной экземпляра @value
.
Обратите внимание, что в этих случаях тип T
не обязательно указывать явно. Поскольку метод инициализации принимает аргумент типа T
, общий параметр можно вывести из использования. Давайте создадим Holder(String)
:
str = Holder.new("Hello")
p str.get # Prints "Hello".
Здесь T
считается строкой, поскольку Holder.new
вызывается с аргументом строкового типа.
Классы-контейнеры из стандартной библиотеки являются универсальными классами, как и определенный нами класс Holder
. Некоторые примеры: Array(T)
, Set(T)
и Hash(K, V)
. Вы можете поиграть с созданием собственных классов контейнеров, используя дженерики.
Далее давайте узнаем, как вызывать и обрабатывать исключения.
Исключения
Существует множество способов, по которым код может сбоить. Некоторые сбои обнаруживаются во время анализа, например, невыполненный метод или нулевое значение в переменной, которое не должно содержать nil
. Некоторые другие сбои происходят во время выполнения программы и описываются специальными объектами: исключениями. Исключение представляет собой сбой на "счастливом пути" и содержит точное местоположение, в котором была обнаружена ошибка, а также подробные сведения для ее понимания.
Исключение может быть вызвано в любой момент с помощью метода верхнего уровня raise. Этот метод ничего не вернет; вместо этого он начнет выполнять обратные вызовы всех методов, как если бы все они имели неявный возврат. Если ничто не фиксирует исключение выше в цепочке методов, программа завершит работу, и пользователю будут представлены подробные сведения об исключении. Приятным аспектом возникновения исключения является то, что оно не должно останавливать выполнение программы; вместо этого его можно перехватить и обработать, возобновив нормальное выполнение.
Давайте рассмотрим пример:
def half(num : Int)
if num.odd?
raise "The number #{num} isn't even"
end
num // 2
end
p half(4) # => 2
p half(5) # Unhandled exception: The number 5 isn't even (Exception)
p half(6) # This won't execute as we have aborted the program.
В предыдущем фрагменте мы определили метод half
, который возвращает половину заданного целого числа, но только для четных чисел. Если задано нечетное число, это вызовет исключение. В этой программе нет ничего, что могло бы перехватить и обработать это исключение, поэтому программа завершит работу с сообщением о необработанном исключении.
Обратите внимание, что raise "описание ошибки"
– это то же самое, что raise Exception. new("описание ошибки")
, поэтому будет создан объект exception. Exception
- это класс, единственная особенность которого заключается в том, что метод raise принимает только его объекты.
Чтобы показать разницу между ошибками во время компиляции и во время выполнения, попробуйте добавить p half("привет")
к предыдущему примеру. Теперь это недопустимая программа (из-за несоответствия типов), и она даже не собирается, поэтому не может быть запущена. Ошибки во время выполнения обнаруживаются и сообщаются только во время выполнения программы.
Исключения могут быть зафиксированы и обработаны с помощью ключевого слова rescue. Оно чаще используется в выражениях begin
и end
, но может использоваться непосредственно в телах методов или блоков. Вот пример:
begin
p half(3)
rescue
puts "can't compute half of 3!"
end
Если внутри выражения begin
возникнет какое-либо исключение, независимо от того, насколько глубоко оно находится в цепочке вызовов метода, это исключение будет восстановлено в коде rescue
. Удобно иметь возможность обрабатывать все виды исключений за один раз, но вы также можете получить доступ к тому, что это за исключение, указав переменную:
begin
p half(3)
rescue error
puts "can't compute half of 3 because of #{error}"
end
Здесь мы зафиксировали объект exception и можем его проверить. Мы могли бы даже вызвать его снова, используя raise error
. Та же концепция может быть применена к телам методов:
def half?(num)
half(num)
rescue
nil
end
p half? 2 # => 1
p half? 3 # => nil
p half? 4 # => 2
В этом примере у нас есть версия метода half
, которая называется half?
. Этот метод возвращает объединение Int32 | Nil,
в зависимости от введенного номера.
Наконец, ключевое слово rescue также можно использовать встроенно, чтобы защитить одну строку кода от любого исключения и заменить ее значение. Метод half?
можно реализовать следующим образом:
def half?(num)
half(num) rescue nil
end
В реальном мире обычной практикой является пойти наоборот и сначала реализовать метод, который возвращает nil
в неудачном пути, а затем создать вариант, который вызывает исключение поверх первой реализации.
Стандартная библиотека содержит множество типов предопределенных исключений, таких как DivisionByZeroError
, IndexError
и JSON::Error
. Каждый из них представляет различные типы ошибок. Это простые классы, которые наследуются от класса Exception
.
Пользовательские исключения
Поскольку исключения - это обычные объекты, а Exception
- это класс, вы можете определять новые типы исключений, наследуя от них. Давайте посмотрим на это на практике:
class OddNumberError