panic!
чи не panic!
Отже, як приймається рішення, коли слід викликати panic!
, а коли повернути Result
? При паніці код не може відновити своє виконання. Можна було б викликати panic!
для будь-якої помилкової ситуації, незалежно від того, чи є спосіб відновлення, чи ні, але з іншого боку, ви приймаєте рішення від імені коду, який викликає, що ситуація необоротна. Коли ви повертаєте значення Result
, ви делегуєте прийняття рішення коду, що викликає. Код, що викликає, може спробувати виконати відновлення способом, який підходить в даній ситуації, або ж він може вирішити, що з помилки в Err
не можна відновитися і викличе panic!
, перетворивши вашу помилку, що виправляється, в невиправну. Тому повернення Result
є гарним вибором за замовчуванням для функції, яка може дати збій.
У таких ситуаціях як приклади, прототипи та тести, більш доречно писати код, який панікує замість повернення Result
. Розгляньмо чому, а потім обговоримо ситуації, коли компілятор не може довести, що помилка неможлива, але ви, як людина, можете це зробити. Глава закінчуватиметься деякими загальними керівними принципами про те, як вирішити, чи варто панікувати в коді бібліотеки.
Приклади, Прототипування та Тести
Коли ви пишете приклад, який ілюструє деяку концепцію, наявність гарного коду обробки помилок може зробити приклад менш зрозумілим. В прикладах виклик методу unwrap
, який може призвести до паніки, є лише позначенням способу обробки помилок у додатку, який може відрізнятися в залежності від того, що робить решта коду.
Так само методи unwrap
та expect
є дуже зручними при створенні прототипу, перш ніж ви будете готові вирішити, як обробляти помилки. Вони залишають чіткі маркери в коді до моменту, коли ви будете готові зробити програму надійнішою.
Якщо в тесті відбувається збій при виклику методу, то ви б хотіли, щоб весь тест не пройшов, навіть якщо цей метод не є функціональністю, що тестується. Оскільки виклик panic!
це спосіб, яким тест позначається як невдалий, використання unwrap
чи expect
– саме те, що потрібно.
Випадки, коли у Вас Більше Інформації, ніж у Компілятора
Також було б доцільно викликати unwrap
або expect
, коли у вас є якась інша логіка, яка гарантує, що Result
буде мати значення Ok
, але вашу логіку не розуміє компілятор. У вас, як і раніше, буде значення Result
, яке потрібно обробити: будь-яка операція, яку ви викликаєте, все ще має можливість невдачі в цілому, хоча це логічно неможливо у вашій конкретній ситуації. Якщо, перевіряючи код вручну, ви можете переконатися, що ніколи не буде варіанту Err
, то можна викликати unwrap
, а ще краще задокументувати причину, з якої ви думаєте, що ніколи не матимете варіант Err
у тексті expect
. Ось приклад:
fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid"); }
Ми створюємо екземпляр IpAddr
шляхом аналізу жорстко заданого рядка. Можна побачити що 127.0.0.1
є дійсною IP-адресою, тому доречно використовувати expect
тут. Однак наявність жорстко заданого правильного рядка не змінює тип повертаємого значення методу parse
: ми все ще отримуємо значення Result
, і компілятор досі змушує нас обробляти Result
так, ніби варіант Err
є можливим, тому що компілятор недостатньо розумний, щоб побачити, що цей рядок завжди є дійсною IP-адресою. Якщо рядок IP-адреси надійшов від користувача, а не є жорстко заданим у програмі, він може призвести до помилки, тому ми точно хотіли б обробити Result
більш надійним способом. Згадка про припущення, що ця IP-адреса жорстко задана, спонукатиме нас до зміни expect
на кращий код обробки помилок, якщо в майбутньому нам знадобиться отримати IP-адресу з іншого джерела.
Настанови з Обробки Помилок
Бажано, щоб код панікував, якщо він може опинитися в некоректному стані. В цьому контексті некоректний стан це такий стан, коли деяке допущення, гарантія, контракт чи інваріант були порушені. Наприклад, коли неприпустимі, суперечливі чи пропущенні значення передаються у ваш код, та інші приклади зі списку нижче:
- Некоректний стан - це щось неочікуване, відмінне від того, що може відбуватися час від часу, наприклад, коли користувач вводить дані у неправильному форматі.
- Ваш код після цієї точки повинен покладатися на те, що він не знаходиться у некоректному стані, замість перевірок наявності проблеми на кожному етапі.
- Немає гарного способу закодувати цю інформацію в типах, які ви використовуєте. Ми подивимося приклад того, що ми маємо на увазі в розділі “Кодування станів та поведінки на основі типів” розділу 17.
Якщо хтось викликає ваш код та передає значення, які не мають сенсу, краще за все повернути помилку, якщо це можливо, щоб користувач бібліотеки мав змогу вирішити, що йому робити в цьому випадку. Однак, у випадках, коли продовження може бути небезпечним чи шкідливим, найкращим вибором може бути виклик panic!
для оповіщення користувача бібліотеки, що в його коді є помилка й він може її виправити. Також panic!
підходить, якщо ви викликаєте зовнішній, неконтрольований вами код, і він повертає неприпустимий стан, який ви не можете виправити.
Однак, якщо очікується збій, краще повернути Result
, ніж виконати виклик panic!
. Як приклад можна привести синтаксичний аналізатор, якому передали неправильно сформовані дані чи статус HTTP-запиту, що повернувся, вказує на те, що ви досягли обмеження частоти запитів. У цих випадках повертання Result
вказує на те, що відмова є очікуваною, такою, яку код, що викликає, повинен вирішити, як саме обробити.
Коли ваш код виконує операцію, яка може бути ризикованою для користувача, якщо використовуються неприпустимі значення, ваш код повинен спочатку перевірити чи вони коректні, та панікувати, якщо це не так. Діяти таким чином рекомендується в основному з міркувань безпеки: спроба оперувати некоректними даними може спричинити вразливість вашого коду. Це основна причина, через що стандартна бібліотека буде викликати panic!
, якщо спробувати отримати доступ до пам'яті поза межами масиву: доступ до пам'яті, яка не стосується поточної структури даних, є відомою проблемою безпеки. Функції часто мають контракти: їх поведінка гарантується, тільки якщо вхідні дані відповідають визначеним вимогам. Паніка при порушенні контракту має сенс, тому що це завжди вказує на дефект з боку коду, що викликає, і це не помилка, яку б ви хотіли, щоб код, що викликає, явно обробляв. Насправді немає розумного способу для відновлення коду, що викликає; Програмісти, що викликають ваш код, повинні виправити свій. Контракти для функції, особливо порушення яких викликає паніку, слід описати в документації API функції.
Проте, наявність великої кількості перевірок помилок у всіх ваших функціях було б багатослівним та дратівливим. На радість, можна використовувати систему типів Rust (отже і перевірку типів компілятором), щоб вона зробила множину перевірок замість вас. Якщо ваша функція має визначений тип в якості параметру, ви можете продовжити роботу з логікою коду знаючи, що компілятор вже забезпечив правильне значення. Наприклад, якщо використовується звичайний тип, а не тип Option
, то ваша програма очікує наявність чогось замість нічого. Ваш код не повинен буде опрацювати обидва варіанти Some
та None
: він буде мати тільки один варіант для певного значення. Код, який намагається нічого не передавати у функцію, не буде навіть компілюватися, тому ваша функція не повинна перевіряти такий випадок під час виконання. Інший приклад - це використання цілого типу без знаку, такого як u32
, який гарантує, що параметр ніколи не буде від'ємним.
Створення Користувацьких Типів для Перевірки
Розвиньмо ідею використання системи типів Rust щоб переконатися, що в нас є коректне значення, та розглянемо створення користувацького типа для валідації. Згадаємо гру вгадування числа з розділу 2, в якому наш код просив користувача вгадати число між 1 й 100. Ми ніколи не перевіряли, що припущення користувача знаходяться в межах цих чисел, перед порівнянням з задуманим нами числом; ми тільки перевіряли, що воно додатне. У цьому випадку наслідки були не дуже страшними: наші повідомлення “Забагато” чи “Замало”, які виводилися у консоль, все одно були коректними. Але було б краще підштовхувати користувача до правильних догадок та мати різну поведінку для випадків, коли користувач пропонує число за межами діапазону, і коли користувач вводить, наприклад, літери замість цифр.
One way to do this would be to parse the guess as an i32
instead of only a u32
to allow potentially negative numbers, and then add a check for the number being in range, like so:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Вираз if
перевіряє, чи знаходиться наше значення поза діапазону, повідомляє користувачу про проблему та викликає continue
, щоб почати наступну ітерацію циклу й попросити ввести інше число. Після виразу if
ми можемо продовжити порівняння значення guess
із задуманим числом, знаючи, що guess
належить діапазону від 1 до 100.
Однак, це не ідеальне рішення: якби було надзвичайно важливо, щоб програма працювала тільки зі значеннями від 1 до 100 та в ній існувало б багато функцій, з такою вимогою, наявність подібної перевірки у кожній функції було б виснажливим (та мало б змогу вплинути на швидкодію).
Замість цього можна створити новий тип та помістити перевірки у функцію створення екземпляру цього типу, не повторюючи їх повсюди. Таким чином, функції можуть використовувати новий тип у своїх сигнатурах та бути впевненими у значеннях, які їм передають. Лістинг 9-13 демонструє один зі способів, як визначити тип Guess
, так щоб екземпляр Guess
створювався лише при умові, що функція new
отримує значення від 1 до 100.
#![allow(unused)] fn main() { pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } pub fn value(&self) -> i32 { self.value } } }
Спочатку ми визначимо структуру з ім'ям Guess
, яка має поле з іменем value
типу i32
. Ось де буде збережено число.
Потім ми реалізуємо асоційовану функцію new
структури Guess
, яка створює нові екземпляри значень типу Guess
. Функція new
має один параметр value
типу i32
та повертає Guess
. Код у тілі функції new
перевіряє, що значення value
знаходиться між 1 та 100. Якщо value
не проходить цю перевірку, ми викликаємо panic!
, що сповістить програміста, який написав код, що в його коді є помилка, яку необхідно виправити, оскільки спроба створення Guess
зі значенням value
поза заданого діапазону порушує контракт, на який покладається Guess::new
. Умови, за яких Guess::new
панікує, повинні бути описані в документації до API; ми розглянемо угоди про документації, що вказують на можливість виникнення panic!
в документації API, яку ви створите в розділі 14. Якщо value
проходить перевірку, ми створюємо новий екземпляр Guess
, у якого значення поля value
дорівнює значенню параметра value
, і повертаємо Guess
.
Потім ми реалізуємо метод з назвою value
, який запозичує self
, не має інших параметрів, та повертає значення типу i32
. Цей метод іноді називають витягувач (getter), тому що його метою є вилучити дані з полів структури та повернути їх. Цей публічний метод є необхідним, оскільки поле value
структури Guess
є приватним. Важливо, щоб поле value
було приватним, щоб код, який використовує структуру Guess
, не міг встановлювати value
напряму: код зовні модуля повинен використовувати функцію Guess::new
для створення екземпляру Guess
, таким чином гарантуючи, що у Guess
немає можливості отримати value
, не перевірене умовами у функції Guess::new
.
Функція, яка отримує або повертає тільки числа від 1 до 100, може оголосити у своїй сигнатурі, що вона отримує чи повертає Guess
, замість i32
, таким чином не буде необхідності робити додаткові перевірки в тілі такої функції.
Підсумок
Можливості обробки помилок в Rust покликані допомогти написанню більш надійного коду. Макрос panic!
сигналізує, що ваша програма знаходиться у стані, яке вона не може обробити, та дозволяє сказати процесу щоб він зупинив своє виконання, замість спроби продовжити виконання з некоректними чи невірними значеннями. Перерахунок (enum) Result
використовує систему типів Rust, щоб повідомити, що операції можуть завершитися невдачею, і ваш код мав змогу відновитися. Можна використовувати Result
, щоб повідомити коду, що викликає, що він повинен обробити потенціальний успіх чи потенційну невдачу. Використання panic!
та Result
правильним чином зробить ваш код більш надійним перед обличчям неминучих помилок.
Тепер, коли ви побачили корисні способи використання узагальнених типів Option
та Result
в стандартній бібліотеці, ми поговоримо про те, як працюють узагальненні типи і як ви можете використовувати їх у власному коді.