В ідеальному світі все працює бездоганно. Але в реальному світі щось може піти не так, і те, як ми поводимося в таких ситуаціях, визначає, чи буде наше програмне забезпечення стійким і надійним. Винятки - це ключове поняття в програмуванні, яке дає нам змогу коректно обробляти помилки та непередбачувані ситуації.
Якщо помилку не обробити, вона зупинить програму й виведе повідомлення про помилку. У більшості випадків ми не хочемо, щоб програма зупинялася, коли виникає помилка; натомість ми хочемо обробити цю помилку й далі виконувати програму.
У Crystal винятки викидають за допомогою ключового слова raise, якому можна передати або String, або обʼєкт Exception.
Якщо їх не обробити, програма зупиниться й надрукує повідомлення про помилку.
raise "This is an error"
У Crystal є кілька вбудованих винятків, як-от ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError та багато інших.
Для них потрібно передати повідомлення.
raise ArgumentError.new("This is an argument error")
Ми ж не хочемо, щоб наша програма аварійно завершувалася, коли виникає виняток.
Тому, коли ми знаємо, що якась частина коду схильна до помилок, ми можемо обгорнути її в блок begin і перехопити виняток блоком rescue.
Блок begin позначає початок коду, який може викинути виняток, а блок rescue обробляє цей виняток.
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
У блоці rescue також можна вказати змінну, щоб отримати обʼєкт винятку.
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
У блоці rescue також можна вказати конкретний тип винятку, щоб перехопити лише його.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end
# or
begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
Можна використовувати кілька блоків rescue, щоб обробити різні типи винятків.
У наведеному нижче прикладі перший блок rescue перехопить ArgumentError, а другий блок rescue перехопить будь-який інший виняток.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
Блок begin також може мати блок else, який виконується, якщо виняток не виник.
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
І нарешті, є блок ensure, який виконується завжди, незалежно від того, чи виник виняток.
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
Деякі методи мають дві версії: одну з ! і одну без.
Це може означати дві різні речі.
Або метод змінює обʼєкт, або метод може викинути виняток.
Але є й інша конвенція щодо завершення назви методу знаком ?, про яку йдеться в концепції булевих значень (англ. Boolean).
Деякі методи типово викидають виняток, але також мають версію, назва якої закінчується на ? і яка повертає nil замість викидання винятку.
Це ідеально підходить, коли ми хочемо уникнути виникнення помилки. Це може допомогти продуктивності, оскільки не потрібно створювати трасування стеку, а за правильного налаштування код може стати безпечнішим.
Ми також можемо створювати власні винятки, успадковуючи їх від класу Exception.
У цьому разі ми можемо за бажанням перевизначити метод initialize, щоб задати повідомлення винятку.
Це можна зробити, присвоївши змінній екземпляра з назвою @message потрібне повідомлення.
class MyException < Exception
def initialize
@message = "This is my exception"
end
end
raise MyException.new
Настав день, на який ми так довго чекали, і тепер ми з гордістю володіємо чудовою фермою в Альпах.
Ми досі не любимо прокидатися надто рано, щоб нагодувати своїх корів.
Оскільки ми чудові інженери, ми будуємо дозатор корму FEED-M-ALL.
Аби завершити проєкт, залишилося написати фрагмент коду, який обчислює кількість корму, що має дістатися кожній корові. Важливо, щоб кожна корова отримала однакову кількість, адже треба уникати конфліктів. Корови дуже чутливі.
На щастя, нам не потрібно самостійно виводити всі формули для обчислення кількості корму. Ми використовуємо якусь загадкову зовнішню бібліотеку, яку знайшли в інтернеті. Вона має зробити корів найщасливішими. Бібліотека надає клас, який реалізує такий інтерфейс. Ми будемо покладатися на це у коді, який пишемо самі.
class FodderCalculator
def fodder_amount! : Number
end
def fattening_factor! : Number
end
end
Працюючи над кодом, ми вдосконалимо обробку помилок, щоб зробити її надійнішою і такою, яку потім буде легше налагоджувати в повсякденному житті на фермі.
Насамперед зосередимося на написанні коду, потрібного для обчислення кількості корму на одну корову.
Реалізуйте метод TheFarm.divide_food, який приймає як аргументи обʼєкт FodderCalculator і кількість корів (ціле число).
У цьому завданні вважаємо, що передана кількість корів завжди більша за нуль.
Метод має повернути кількість корму на одну корову.
Щоб виконати обчислення, спершу треба отримати загальну кількість корму для всіх корів.
Для цього викликаємо метод fodder_amount! і передаємо кількість корів.
Крім того, потрібен коефіцієнт, на який цю кількість треба помножити.
Цей коефіцієнт отримуємо, викликавши метод fattening_factor!.
Маючи ці два значення та кількість корів, тепер можемо обчислити кількість корму на одну корову (як float64).
Якщо один із викликаних методів повертає помилку, виконання має зупинитися й повернути 0.
fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0
fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0
Працюючи над першим завданням, ми зрозуміли, що зовнішня бібліотека, яку ми використовуємо, не така якісна, як здавалося. Наприклад, вона не вміє належно обробляти некоректні вхідні дані. Ми хочемо обійти це обмеження, додавши перевірку вхідного значення у власному коді.
Напишіть функцію TheFarm.validate_input_and_divide_food з тією самою сигнатурою, що й TheFarm.divide_food вище.
TheFarm.divide_food і повернути результат цього виклику."Number of cows must be greater than 0".TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15
TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0
Перевірка кількості корів перед передаванням була слушним кроком, але нас не зовсім тішить неконкретне повідомлення про помилку.
Ми вирішуємо зробити краще й створюємо власний клас помилки InvalidCowsError.
Власна помилка має зберігати кількість корів (Int32) і власне повідомлення (String), а метод Expection має серіалізувати дані в такому форматі:
{number of cows} cows are invalid: {custom message}
Озброївшись власною помилкою, реалізуйте функцію TheFarm.validate_number_of_cows, яка приймає кількість корів як ціле число й повертає помилку (або nil).
InvalidCowsError з власним повідомленням "there are no negative cows".InvalidCowsError з власним повідомленням "no cows don't need food".nil, що означає, що перевірка успішна.TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
Після нелегкої роботи зі створення цієї функції перевірки ми помічаємо, що вже вечір, і залишаємо робоче місце, щоб помилуватися заходом сонця над горами. А додавання нової функції перевірки до коду залишаємо на інший день.
Зареєструйтеся на Exercism, щоб вивчати й опановувати Crystal, а також 26 концепцій142 вправи та справжнє наставництво від людей, і все це безкоштовно.