Помилки, що піддаються відновленню за допомогою 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");
}

Блок коду 9-3: Відкривання файлу

Типом, який повертає 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),
    };
}

Блок коду 9-4: Використання match виразу для обробки варіантів Result

Звернуть увагу, що аналогічно до енума 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);
            }
        },
    };
}

Блок коду 9-5: Обробка різних типів помилок різним способом

Тип значення, який повертає 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),
    }
}
}

Блок коду 9-6: Функція, яка повертає помилки в код, який її викликає за допомогою виразу match

Вказану функцію також можливо написати коротшим способом, але ми почнемо з того, що зробимо більшу частину самостійно, для того, щоб познайомитися з обробкою помилок. В кінці ми продемонструємо коротший спосіб. Давайте спочатку розглянемо тип значення, яке повертає функція: 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)
}
}

Listing 9-7: Функція, яка повертає помилки коду, який її викликає за допомогою оператора ?

Якщо розмістити оператор ? після значення 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)
}
}

Блок коду 9-8: Ланцюжок викликів методів після оператора ?

Ми перенесли створення нового екземпляру 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")
}
}

Listing 9-9: Використання методу fs::read_to_string замість того, щоб відкривати файл і потім виконувати його зчитування

Зчитування файлу в стрічку є досить типовою операцією, тому стандартна бібліотека надає зручнішу функцію 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")?;
}

Блок коду 9-10: Спроба використати оператор ? всередині функції main, яка не скомпілюється, оскільки має повертати несумісний тип ()

Цей код відкриває файл, тому ця операція може виконатись не успішно. Оператор ? слідує за значенням 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);
}

Блок коду 9-11: Використання оператора ? на значенні Option<T>

Ця функція повертає 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(())
}

Блок коду 9-12: Зміна функції main щоб повертати Result<(), E> і мати можливість використання оператора ? на значеннях Result

Тип 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, повернімось до теми, яким чином визначати, що з переліченого доцільно використовувати та в яких випадках.