Помилки, що піддаються відновленню за допомогою Result
Більшість помилок не є достатньо серйозними, щоб вимагати повної зупинки виконання програми. Іноді, якщо функція не спрацьовує, то це можна досить просто пояснити й відреагувати певним чином. Наприклад, якщо ви намагаєтесь відкрити файл і ця операція зазнає невдачі, тому що такого файлу немає, то замість завершення процесу ви б захотіли створити такий файл.
Нагадаємо з підрозділу Керування потенційною невдачею за допомогою типу Result
в розділі 2, що Result
визначається як енум, що має два можливих значення Ok
та Err
, наступним чином:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
The T
and E
це параметри, що відносять до узагальнених типів, які ми розглянемо більш детально у розділі 10. Все, що вам необхідно знати на даний момент, що T
представляє тип значення, яке буде повернуто результатом успішного виконання як вміст варіанту Ok
, а E
представляє тип помилки, що буде повернуто як вміст варіанту Err
у випадку невдачі. Оскільки Result
містить ці узагальнені типи параметрів, ми можемо використовувати тип Result
і функції, що визначені для нього у стандартній бібліотеці, для різних випадків, коли значення успішного виконання і значення невдачі, які ми хочемо повернути, можуть відрізнятися.
Спробуймо викликати функцію, яка повертає значення типу Result
, оскільки ця функція може не спрацювати. В блоці коду 9-3 ми спробуємо відкрити файл.
Файл: src/main.rs
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); }
Типом, який повертає File::open
є Result<T, E>
. Узагальнений параметр T
був визначений в реалізації File::open
, як тип значення при успіху при обробці файлу, а саме std::fs::File
. Тип E
, що використовується, як значення помилки, визначений як std::io::Error
. Цей тип значення, що повертається, означає, що виклик File::open
може бути успішним і повернути обробник файлу, за допомогою якого ми можемо його зчитувати, або записувати. Також виклик функції може завершитися не успішно, наприклад файлу може ще не існувати, або у нас не буде дозволів на обробку цього файлу. Функція File::open
має мати можливість сповістити нас, чи виклик був успішним чи ні, і дати нам або обробник файлу, або інформацію про помилку. Ця інформація і є безпосередньо тим, що являє собою енум Result
.
У випадку, коли виклик File::open
був успішним, значенням змінної greeting_file_result
буде екземпляр Ok
, що містить обробник файлу. А у випадку помилки, значенням greeting_file_result
буде екземпляр Err
, який містить інформацію про тим помилкової ситуації, що сталася.
Ми повинні розширити блок коду 9-3, щоб зрозуміти різні підходи в залежності від значення, яке повертає File::open
. Блок коду 9-4 демонструє один із способів обробки Result
, використовуючи базові підхід, такий як вираз співставлення зі зразком (match
), що розглядався у розділі 6.
Файл: src/main.rs
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), }; }
Звернуть увагу, що аналогічно до енума Option
, енум Result
і його варіанти вже введені в область видимості прелюдії, тому немає необхідності вказувати Result::
перед варіантами Ok
і Err
в рукавах виразу співставлення зі зразком match
.
Коли результат буде рівний Ok
, нам необхідно повернути внутрішнє значення file
з варіанту Ok
, таким чином при присвоїмо значення обробника файлу змінній greeting_file
. Після виразу match
ми можемо використовувати обробник файлу для запису чи зчитування.
Другий рукав виразу match
обробляє випадок, коли отримуємо значення Err
результатом виконання File::open
. В нашому прикладі ми вирішили викликати макрос panic!
. Якщо в поточному каталозі немає файлу з іменем hello.txt і ми запустимо наш код, то завдяки макрокоманді panic!
побачимо наступний вивід:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Як завжди, цей вивід розкаже нам, що саме пішло не так.
Застосування виразу match для різних помилок
Код у блоці 9-4 буде завершиться панікою, незалежно від того, чому виклик File::open
не спрацював. Однак, ми б хотіли виконати різні дії для різних причин неуспішного виконання: якщо File::open
не відпрацьовує, оскільки файл не існує, ми б хотіли створити такий файл і повернути обробник для цього нового файлу. Якщо ж File::open
не спрацював через будь-які інші причини, наприклад, у нас немає дозволів для відкриття файлу, ми б все ж таки хотіли викликати panic!
таким самим чином, як це було в блоці коду 9-4. Для цього ми додамо вкладений вираз match
, як показано у блоці коду 9-5.
Файл: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error);
}
},
};
}
Тип значення, який повертає File::open
всередині варіантів Err
є io::Error
, який в свою чергу є структурою, що поставляється стандартною бібліотекою. Ця структура має метод kind
, при виклику якого отримаємо io::ErrorKind
значення. Енум io::ErrorKind
поставляється у стандартній бібліотеці і має варіанти, які представляють різні типи помилок, що можуть бути результатом операції io
. Варіант, який ми хочемо використати, це ErrorKind::NotFound
. Цей варіант сигналізує нам, що файлу, який ми намагаємось відкрити, не існує. Тому ми застосовуємо вираз match у greeting_file_result
, а також ми маємо вкладений вираз match для error.kind()
.
У внутрішньому виразі match ми хочемо перевірити, чи значення, що повертає метод error.kind()
є варіантом NotFound
енума ErrorKind
. Якщо ж так і є, ми пробуємо створити такий файл за допомогою методу File::create
. Однак, оскільки метод File::create
може також завершитися не успішно, нам треба ще один рукав всередині вкладеного виразу match
. Якщо файл не може бути створено, то виводимо інше повідомлення про помилку. Другий рукав зовнішнього виразу match
залишається незмінним, тому програма підіймає паніку на будь-які інші помилки за виключенням помилки відсутнього файлу.
Альтернативи використанню виразу
match
для значень типуResult<T, E>
Схоже, що у нас забагато
match
! Виразmatch
дуже корисний, проте дуже примітивний. У розділі 13 ми будемо вивчати замикання, які використовуються у комбінації з багатьма методами, які визначені для типуResult<T, E>
. Ці методи можуть бути більш виразними за використання виразуmatch
, коли працюємо зі значеннямиResult<T, E>
у своєму коді.Прикладом може бути інший спосіб описати таку ж саму логіку, що показана у блоці коду 9-5, але з використанням замикань і методу
unwrap_or_else
:use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {:?}", error); }) } else { panic!("Problem opening the file: {:?}", error); } }); }
Хоча цей код має таку ж поведінку, що і код у блоці 9-5, він не містить жодного виразу
match
і зрозуміліший для читання. Повертайтесь до цього прикладу після того, як прочитаєте розділ 13 і познайомтесь з методомunwrap_or_else
у документації до стандартної бібліотеки. Також багато інших методів можуть допомогти справитися з великою кількістю вкладений між собою виразівmatch
, при роботі з помилками.
Короткі форми для паніки на помилках: unwrap
і expect
Використання виразу match
працює достатньо добре, але може бути занадто багатослівним і не завжди добре передавати наші наміри. Тип Result<T, E>
має багато допоміжних методів, які визначені для того, щоб здійснити більш специфічні обробки. Метод unwrap
є скороченням імплементації виразу match
, як це було зроблено у блоці коду 9-4. Якщо значення Result
є варіантом Ok
, метод unwrap
поверне значення, як міститься всередині Ok
. Якщо ж Result
є варіантом Err
, то метод unwrap
викличе макрос panic!
для нас. Ось приклад методу unwrap
у дії:
Файл: src/main.rs
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
Якщо ми виконаємо код без існуючого файлу hello.txt, ми отримаємо повідомлення про помилку із виклику panic!
, який здійснить метод unwrap
:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:4:49
Аналогічний метод expect
дозволяє додатково нам вибрати повідомлення про помилку для макрокоманди panic!
. Використання методу expect
замість unwrap
разом із заданням хороших повідомлень про помилку допоможе краще передати ваші наміри й спростить відстежування причин такої паніки. Синтаксис методу expect
виглядає наступним чином:
Файл: src/main.rs
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
Ми використовуємо expect
в такий самий спосіб, як і unwrap
, щоб повернути обробник файлу або викликати макрос panic!
. Повідомленням про помилку, що використовує метод expect
у виклику макросу panic!
, буде параметром, який ми передаємо у expect
, замість стандартного повідомлення макросу panic!
, яку використовує unwrap
. Ось як це виглядає:
thread 'main' panicked at 'hello.txt should be included in this project: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10
Частіше програмісти на Rust віддають перевагу у своєму коді методу expect
аніж unwrap
, додаючи більше контексту з роз'ясненням, чому дана операція має бути завжди успішною для виконання. Таким чином, навіть якщо ваші припущення були не до кінця точними у такій ситуації, у вас буде більше інформації під час відлагодження.
Поширення помилок
Коли ви пишите функцію, імплементація якої може викликати щось, що може не спрацювати, тоді замість обробки помилки всередині функції, ви можете повернути помилку тій частині коду, що її викликала. І тоді вже ця частина коду буде вирішувати, що робити з цією помилкою. Це називається розповсюдженням помилки й дає більше контролю викликаючому коду, де, можливо, є більше інформації або логіки, які диктують, як ця помилка має бути оброблено, ніж те що є доступним в контексті вашого коду функції.
Для прикладу блок коду 9-6 показує функцію, яка зчитає username з файлу. Якщо ж файл не існує або його неможливо прочитати, то цю функція поверне ці помилки в код, який викликає дану функцію.
Файл: src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } }
Вказану функцію також можливо написати коротшим способом, але ми почнемо з того, що зробимо більшу частину самостійно, для того, щоб познайомитися з обробкою помилок. В кінці ми продемонструємо коротший спосіб. Давайте спочатку розглянемо тип значення, яке повертає функція: Result<String, io::Error>
. Це означає, що функція повертає значення типу Result<T, E>
, де узагальнений параметр T
був підставлений конкретним типом String
, а узагальнений тип E
конкретним типом io::Error
.
Якщо виклик функції відпрацює успішно без жодних проблем, то код, який викликає її, отримає значення типу Ok
, яке містить String
- тобто username, який був зчитаним функцією з файлу. Якщо ж виконання функції зіткнеться з якимось проблемами, код,який викликав її отримає значення Err
, яке містить екземпляр io::Error
, який, в свою чергу, містить більше інформації стосовно характеру проблем. Ми вибрали io::Error
як тип значення, що повертається з неї, тому що вона є типом помилок обох функцій, що можуть виконатись не успішно, які ми викликаємо в тілі нашої функції: функція File::open
і read_to_string
.
Тіло функції починається з виклику методу File::open
. Далі ми обробляємо значення Result
за допомогою виразу match
, схоже до того, що було у блоці коду 9-4. Якщо виклик File::open
буде успішним, тоді обробник файлу буде міститися у змінній file
виразу співставлення, який стане значення мутабельної змінної username_file
і виконання функції буде продовжуватися. А у випадку значення Err
, замість виклику panic!
, ми використовуємо ключове слово return
для передчасного виходу з функції з поверненням значення помилки в місце виклику нашої функції, яку отримаємо з виклику File::open
як внутрішню змінну зіставлення e
.
Якщо ми маємо обробник файлу у змінній username_file
, тоді функція створить нове значення String
у змінній username
і викличе метод read_to_string
на обробнику файлу username_file
, щоб прочитати контент цього файлу у значення змінної username
. Метод read_to_string
також повертає Result
, оскільки може виконатись не успішно, навіть виконання File::open
було успішним до цього. Тому нам потрібно ще один вираз match
для обробки цього Result
: якщо read_to_string
був успішним, то і виконання нашої функції теж успішне і повертаємо значення username з файлу, огорнутим у Ok
. Якщо є read_to_string
виконалось не успішно, ми просто повертаємо помилку у той самий спосіб, як і у виразі match
, що обробляв значення виклику File::open
. Однак нам непотрібно явно використовувати return
, оскільки це останній вираз нашої функції.
Код, який викликає цей має обробити отримані або значення Ok
, що містить username або значення Err
, яке містить io::Error
. Ми не повинні знати, що саме код який викликає буде робити з отриманими значеннями. Якщо він отримає значення Err
, то може або викликати panic!
і зупинити виконання програми, або скористатися іменем користувача по замовчуванню, або знайти його де інде. Ми не маємо достатньої інформації стосовно того, що саме код, який викликає буде робити, тому ми поширюємо всю інформацію, як і успішного виконання, так і не успішного вгору, для обробки її належним чином.
Цей патерн поширення помилок є дуже поширеним в Rust, тому Rust має спеціальний оператор знаку питання ?
, для роботи з цим більш зручний спосіб.
Коротка форма поширення помилок оператором ?
Блок коду 9-7 демонстрував імплементацію функції read_username_from_file
, яка має таку ж функціональність, як і функція в блоці коду 9-6, але дана реалізація використовувала оператор ?
.
Файл: src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io; use std::io::Read; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } }
Якщо розмістити оператор ?
після значення Result
, то він буде працювати таким самим чином, як і вираз match
, який ми визначали для обробки значення Result
в блоці коду 9-6. Якщо значення Result
є Ok
, то значення, що знаходиться всередині Ok
, буде повернутим як результат виразу і програма продовжить виконання. Якщо ж значення є Err
, то в цілому з функції буде повернуто Err
, так ніби ми використали ключове слово return
і значення помилки буде передано функції, що викликала даний код.
Є певна різниця між тим, що виконує вираз match
з блоку коду 9-6 і тим, що виконує оператор ?
. Значення помилок, при яких викликається оператор ?
проходять через виклик функції from
, яка визначена на трейті From
стандартної бібліотеки і використовується для конвертації значень із одного типу в інший. Коли оператор ?
викликає функцію from
, отриманий тип помилки конвертується в тим помилки, який був визначений типом, що повертається з поточної функції. Це корисно, коли функція повертає один тип помилки, який являє собою всі можливі шляхи, при яких функція може виконатись не успішно, навіть якщо її частини можуть завершуватись не успішно з різних причин.
Для прикладу, ми б могли змінити функцію read_username_from_file
в блоці коду 9-7, щоб вона повертала кастомізований тип помилки визначений нами, який б називався OurError
. Якщо ми також визначимо імплементацію impl From
для типу OurError
при створенні екземпляру OurError
із io::Error
, тоді виклик оператора ?
в тілі функції read_username_from_file
викличе метод <0>from</0> і здійснить конвертацію типу помилки без необхідності додавання жодного коду у нашу функцію.
В контексті блоку коду 9-7, оператор ?
в кінці виклику функції File::open
поверне значення значення всередині Ok
у змінну username_file
. Якщо ж помилка виникне, то оператор ?
припинить виконання функції заздалегідь і поверне якесь значення Err
коду, який її викликав. Те ж саме буде справедливим для оператора ?
в кінці виклику методу read_to_string
.
Оператор ?
дозволяє уникнути надлишкового коду у функціях і робить їх імплементацію простішою. Ми можемо навіть ще більше скоротити код, об'єднуючи виклики методів в ланцюжок відразу після оператора ?
, як це показано в блоці коду 9-8.
Файл: src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io; use std::io::Read; fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } }
Ми перенесли створення нового екземпляру String
в username
на початок функції. Замість створення змінної username_file
, ми приєднали ланцюжком виклик read_to_string
прямо до результату виклику File::open("hello.txt")?
. Ми все ще маємо оператор ?
в кінці виклику read_to_string
і все ще повертаємо значення Ok
, яке містить username
, якщо обидва виклики File::open
і read_to_string
завершаться успішно, а не повертаємо помилки. Ця функціональність знову ж таки аналогічна тій, що представлена у блоках коду 9-6 і 9-7 з однією тільки відмінністю, що такий шлях більш ергономічний для написання.
Блок коду 9-9 демонструє ще коротший шлях використання fs::read_to_string
.
Файл: src/main.rs
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
Зчитування файлу в стрічку є досить типовою операцією, тому стандартна бібліотека надає зручнішу функцію fs::read_to_string
, яка відкриває файл, створює новий екземпляр String
, зчитує вміст фалу, поміщає вміст в створений екземпляр стрічки String
і повертає його. Звичайно, використання методу fs::read_to_string
не дає можливості пояснити всі підходи до обробки помилок, тому ми спочатку пішли більш довгим шляхом.
Де можна використовувати оператор ?
Оператор ?
може бути використаним тільки у функціях, які повертають тип, який сумісний зі значення, яке він може обробити. Це тому, що оператор ?
створений для обробки раннього повернення значення з функції в такий самий спосіб, як і вираз match
, описаний у блоці коду 9-6. Тут вираз match
використовує значення Result
і повертає значення Err(e)
по рукаву раннього виходу. Типом, що повертається з функції має бути тип Result
, що є сумісним цим return
.
Давайте розглянемо в блоці коду 9-10 помилку, яку отримаємо, якщо використаємо оператор ?
у функції main
, яка має повертати тип, що не сумісний з типом значення, яке ми використовуємо з оператором ?
:
Файл: src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
Цей код відкриває файл, тому ця операція може виконатись не успішно. Оператор ?
слідує за значенням Result
, який повертає File::open
, але функція main
має повертати тип ()
, а не тип Result
. Коли ми спробуємо скомпілювати цей код, ми отримаємо наступне повідомлення про помилку компілювання:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | / fn main() {
4 | | let greeting_file = File::open("hello.txt")?;
| | ^ cannot use the `?` operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error
Ця помилка компілювання вказує на те, що ми можемо використовувати оператор ?
тільки у функціях, які повертають Result
, Option
, або інший тип, який імлементує FromResidual
.
Для виправлення цієї помилки ми маємо два шляхи. Перший полягає в тому, щоб змінювати тип значення, яке повертаємо для нашої функції, щоб бути сумісним по типу зі значенням, яке використовуємо з оператором ?
до того моменту, поки немає жодних інших обмежень для цього. Інший полягає в тому, щоб використовувати вираз match
або один із методів визначених для типу Result<T, E>
, щоб обробити значення Result<T, E>
більш підходящим способом.
Помилка компілювання також говорить, що оператор ?
також можна використовувати зі значенням Option<T>
. Як і з використанням ?
на Result
, ми можемо використовувати оператор ?
на Option
у функціях, які повертають Option
. Поведінка оператора ?
, коли викликаємо його на Option<T>
є подібною до випадку з Result<T, E>
: якщо значення None
, то це значення буде повернуто достроково з функції. Якщо ж значення Some
, то значення всередині Some
буде значенням результату виразу і виконання функції буде продовжуватися далі. Блок коду 9-11 є прикладом функції, що знаходить останній символ в отриманому тексті:
fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } fn main() { assert_eq!( last_char_of_first_line("Hello, world\nHow are you today?"), Some('d') ); assert_eq!(last_char_of_first_line(""), None); assert_eq!(last_char_of_first_line("\nhi"), None); }
Ця функція повертає Option<char>
, тому що як є можливість, що символ може бути, так і можливість, що символу не буде. Ця функція приймає як аргумент зріз стрічки text
і викликає метод lines
на ньому, який повертає ітератор над рядками у стрічці. Оскільки ця функція має отримати перший рядок, вона викликає метод next
на ітераторі, щоб отримати перше значення з ітератора. Якщо text
буде пустою стрічкою, то виклик next
поверне значення None
, для цього випадку ми використовуємо оператор ?
, щоб достроково зупинити виконання і повернути None
з функції last_char_of_first_line
. Якщо ж text
не пуста стрічка, виклик next
поверне значення Some
, яке буде містити зріз стрічки, як перший рядок у text
.
Оператор ?
вилучає цей зріз стрічки і ми можемо далі викликати метод chars
на цьому зрізі, щоб отримати ітератор символів. Ми зацікавлені в останньому символі першої стрічки, тому ми викликаємо метод last
, для останнього елементу ітератора. Це значення також Option
, оскільки можливо, що цей перший рядок є пустою стрічкою, наприклад, якщо text
починається з пустого рядку, але має символи на наступних рядках, як, до прикладу, у "\nhi"
. Однак, якщо є останній символ у першому рядку, то він повернеться загорнутим у Some
. Оператор ?
посередині дає нам виразний спосіб описати логіку, змушуючи нас реалізовувати тіло функції в один рядок. Якщо б ми не використовували оператор ?
на Option
, то довелося би реалізовувати логіку з використанням більшої кількості викликів методів та виразів match
.
Варто зазначити, що ми можемо використовувати оператор ?
на Result
всередині функцій, які повертають Result
, а також можемо використовувати на Option
у функціях, які повертають Option
, але ми не можемо їх змішувати і порівнювати. Оператор ?
не може автоматично конвертувати Result
в Option
або навпаки. В цих випадках слід використовувати методи на зразок ok
на Result
, або ok_or
на Option
, для здійснення явного конвертування.
Поки що всі функції main
, які ми використовували повертали значення типу ()
. Функція main
є спеціальною, тому що є вхідною та вихідною точкою для запуску програм і має строгі обмеження до типу значення, яке вона повертає, щоб програма поводила себе так, як очікується.
На щастя, main
може також повертати значення типу Result<(), E>
. Блок 9-12 містить код з блоку 9-10, але тут ми змінили тип значення, яке повертається з main
на Result<(), Box<dyn Error>>
і повернули значення Ok(())
в кінці тіла функції. Цей код буде тепер компілюватися:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Тип Box<dyn Error>
є об'єктом типажом, про який ми будемо говорити у секції «Використання трейт об'єктів, які допускають значення різних типів» розділу 17. На тепер ми можемо читати це, якBox<dyn Error>
, що означає «будь який тип помилок». Використання оператора ?
на значенні Result
всередині функції main
з помилкою типу Box<dyn Error>
є допустимим, оскільки допустимими є будь-які значення Err
для дострокового повернення. Навіть якщо тіло цієї функції main
буде повертати помилки типу std::io::Error
, спеціалізована Box<dyn Error>
сигнатура буде залишатися коректною, навіть якщо додамо більше коду в тіло функції main
, який може повертати помилки іншого типу.
Коли функція main
повертає Result<(), E>
, виконання запиниться зі значенням 0
, якщо main
поверне Ok(())
і запиниться з ненульовим значенням, якщо main
поверне значення Err
. Виконувані файли, написані на C повертають цілі числа коли завершуються: програми які виконалися успішно повертають ціле число 0
, а програми що виконалися з помилкою повертають цілі числа, відмінні від 0
. Rust також повертає цілі числа з виконуваних файлів, щоб бути сумісним з такою домовленістю.
Функція main
може повертати довільний тип, який імплементує std::process::Termination
трейт, що містить метод report
, який повертає ExitCode
. Для більшого розуміння імплементації трейту Termination
на власних типах рекомендуємо ознайомитися з документацією до стандартної бібліотеки.
Тепер, коли ми обговорили деталі виклику panic!
й використанню Result
, повернімось до теми, яким чином визначати, що з переліченого доцільно використовувати та в яких випадках.