Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Восстанавливаемые ошибки с Result

Большинство ошибок не настолько серьёзны, чтобы требовать полной остановки программы. Иногда, когда функция завершается с ошибкой, причина легко интерпретируется и на неё можно отреагировать. Например, если вы пытаетесь открыть файл и эта операция завершается с ошибкой, потому что файл не существует, вы, возможно, захотите создать файл вместо завершения процесса.

Вспомните из главы 2 “Обработка потенциального сбоя с Result, что перечисление Result определено с двумя вариантами, Ok и Err, следующим образом:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

T и E — это параметры обобщённых типов: мы обсудим обобщения подробнее в главе 10. Сейчас вам нужно знать, что T представляет тип значения, которое будет возвращено в случае успеха внутри варианта Ok, а E представляет тип ошибки, который будет возвращён в случае сбоя внутри варианта Err. Поскольку Result имеет эти параметры обобщённых типов, мы можем использовать тип Result и определенные на нём функции во многих различных ситуациях, где возвращаемые значения успеха и ошибки могут отличаться.

Вызовем функцию, возвращающую значение Result, потому что функция может завершиться с ошибкой. В листинге 9-3 мы пытаемся открыть файл.

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}
Listing 9-3: Открытие файла

Тип возвращаемого значения File::open — это Result<T, E>. Параметр обобщённого типа T был заполнен реализацией File::open типом значения успеха, std::fs::File, который является дескриптором файла. Тип E, используемый в значении ошибки, — это std::io::Error. Этот тип возврата означает, что вызов File::open может завершиться успешно и вернуть дескриптор файла, из которого можно читать или в который можно писать. Вызов функции также может завершиться с ошибкой: например, файл может не существовать или у вас может не быть прав доступа к файлу. Функция File::open должна иметь способ сообщить нам, succeeded ли она, и одновременно дать либо дескриптор файла, либо информацию об ошибке. Эта информация именно то, что передаёт перечисление Result.

В случае, когда File::open завершается успешно, значение в переменной greeting_file_result будет экземпляром Ok, содержащим дескриптор файла. В случае, когда она завершается с ошибкой, значение в greeting_file_result будет экземпляром Err, содержащим дополнительную информацию о том, какая ошибка произошла.

Нам нужно добавить в код из листинга 9-3 логику для выполнения различных действий в зависимости от возвращаемого значения File::open. Листинг 9-4 показывает один из способов обработки Result с помощью базового инструмента — выражения match, которое мы обсуждали в главе 6.

Filename: 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:?}"),
    };
}
Listing 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` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`

thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Как обычно, этот вывод точно сообщает нам, что пошло не так.

Сопоставление с разными ошибками

Код в листинге 9-4 вызовет panic! независимо от причины сбоя File::open. Однако мы хотим предпринимать разные действия для разных причин сбоя. Если File::open завершился с ошибкой потому, что файл не существует, мы хотим создать файл и вернуть дескриптор нового файла. Если File::open завершился с ошибкой по любой другой причине — например, потому что у нас нет прав на открытие файла — мы всё ещё хотим, чтобы код вызвал panic! так же, как это было в листинге 9-4. Для этого мы добавляем вложенное выражение match, показанное в листинге 9-5.

Filename: 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:?}"),
            },
            _ => {
                panic!("Problem opening the file: {error:?}");
            }
        },
    };
}
Listing 9-5: Обработка разных видов ошибок разными способами

Тип значения, которое File::open возвращает внутри варианта Err, — это io::Error, который является структурой, предоставляемой стандартной библиотекой. У этой структуры есть метод kind, который мы можем вызвать, чтобы получить значение io::ErrorKind. Перечисление io::ErrorKind предоставляется стандартной библиотекой и имеет варианты, представляющие различные виды ошибок, которые могут возникнуть в результате операции io. Вариант, который мы хотим использовать, — это ErrorKind::NotFound, который указывает, что файл, который мы пытаемся открыть, ещё не существует. Поэтому мы сопоставляем greeting_file_result, но также имеем вложенное сопоставление по 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 в действии:

Filename: 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 src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Аналогично, метод expect позволяет нам также выбрать сообщение об ошибке panic!. Использование expect вместо unwrap и предоставление хороших сообщений об ошибках может передать ваше намерение и облегчить отслеживание источника паники. Синтаксис expect выглядит так:

Filename: 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 src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }

В коде production-quality большинство Rustaceans выбирают expect вместо unwrap и дают больше контекста о том, почему операция должна всегда завершаться успешно. Таким образом, если ваши предположения когда-либо окажутся неверными, у вас будет больше информации для отладки.

Распространение ошибок

Когда реализация функции вызывает что-то, что может завершиться с ошибкой, вместо обработки ошибки внутри самой функции вы можете вернуть ошибку в вызывающий код, чтобы он мог решить, что делать. Это известно как распространение ошибки и даёт больше контроля вызывающему коду, где может быть больше информации или логики, определяющей, как должна обрабатываться ошибка, чем то, что у вас доступно в контексте вашего кода.

Например, листинг 9-6 показывает функцию, которая читает имя пользователя из файла. Если файл не существует или не может быть прочитан, эта функция вернёт эти ошибки в код, который вызвал функцию.

Filename: 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),
    }
}
}
Listing 9-6: Функция, которая возвращает ошибки в вызывающий код с использованием match

Эту функцию можно написать гораздо короче, но мы начнём с того, что сделаем многое вручную, чтобы исследовать обработку ошибок; в конце мы покажем более короткий способ. Сначала посмотрим на тип возврата функции: Result<String, io::Error>. Это означает, что функция возвращает значение типа Result<T, E>, где параметр обобщённого типа T был заполнен конкретным типом String, а параметр обобщённого типа E был заполнен конкретным типом io::Error.

Если эта функция завершается успешно без каких-либо проблем, код, который вызывает эту функцию, получит значение Ok, содержащее Stringusername, который эта функция прочитала из файла. Если эта функция столкнётся с какими-либо проблемами, вызывающий код получит значение Err, содержащее экземпляр io::Error, который содержит дополнительную информацию о том, какими были проблемы. Мы выбрали io::Error в качестве типа возврата этой функции, потому что это как раз тип значения ошибки, возвращаемого из обеих операций, которые мы вызываем в теле этой функции и которые могут завершиться с ошибкой: функции File::open и метода read_to_string.

Тело функции начинается с вызова функции File::open. Затем мы обрабатываем значение Result с помощью match, аналогично 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, содержащее имя пользователя, либо значение Err, содержащее io::Error. Решать, что делать с этими значениями, должен вызывающий код. Если вызывающий код получает значение Err, он может вызвать panic! и завершить программу, использовать имя пользователя по умолчанию или найти имя пользователя в другом месте, кроме файла, например. У нас недостаточно информации о том, что на самом деле пытается сделать вызывающий код, поэтому мы распространяем всю информацию об успехе или ошибке вверх для соответствующей обработки.

Этот шаблон распространения ошибок настолько распространён в Rust, что Rust предоставляет оператор ?, чтобы упростить это.

Ярлык для распространения ошибок: оператор ?

Листинг 9-7 показывает реализацию read_username_from_file, которая имеет ту же функциональность, что и в листинге 9-6, но эта реализация использует оператор ?.

Filename: 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 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<io::Error> for OurError для создания экземпляра OurError из io::Error, то вызовы оператора ? в теле read_username_from_file будут вызывать from и преобразовывать типы ошибок без необходимости добавлять дополнительный код в функцию.

В контексте листинга 9-7 ? в конце вызова File::open вернёт значение внутри Ok в переменную username_file. Если произойдёт ошибка, оператор ? досрочно выйдет из всей функции и передаст любое значение Err в вызывающий код. То же самое применяется к ? в конце вызова read_to_string.

Оператор `устраняет много шаблонного кода и делает реализацию этой функции проще. Мы могли бы сделать этот код ещё короче, цепляя вызовы методов сразу после?`, как показано в листинге 9-8.

Filename: 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 mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}
Listing 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.

Filename: 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. В листинге 9-6 match использовал значение Result, а ветвь досрочного возврата возвращала значение Err(e). Тип возврата функции должен быть Result, чтобы быть совместимым с этим return.

В листинге 9-10 посмотрим на ошибку, которую мы получим, если используем оператор ? в функции main с типом возврата, несовместимым с типом значения, на котором мы используем ?.

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}
Listing 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() {
  | --------- this function should return `Result` or `Option` to accept `?`
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
help: consider adding return type
  |
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 |     let greeting_file = File::open("hello.txt")?;
5 +     Ok(())
  |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

Эта ошибка указывает, что мы можем использовать оператор ? только в функции, которая возвращает Result, Option или другой тип, который реализует FromResidual.

Чтобы исправить ошибку, у вас есть два выбора. Один выбор — изменить тип возврата вашей функции, чтобы он был совместим со значением, на котором вы используете оператор ?, если у вас нет ограничений, предотвращающих это. Другой выбор — использовать match или один из методов Result<T, E> для обработки Result<T, E> любым подходящим способом.

Сообщение об ошибке также упомянуло, что ? можно использовать со значениями Option<T>. Как и при использовании ? на Result, вы можете использовать ? на Option только в функции, которая возвращает Option. Поведение оператора ? при вызове на Option<T> похоже на его поведение при вызове на Result<T, E>: если значение — None, 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);
}
Listing 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(()) в конец. Этот код теперь скомпилируется.

Filename: src/main.rs
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}
Listing 9-12: Изменение main на возврат Result<(), E> позволяет использовать оператор ? на значениях Result.

Тип Box<dyn Error> — это объект типажа, о котором мы поговорим в “Использовании объектов типажей, которые позволяют иметь значения разных типов” в главе 18. Пока вы можете читать 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, вернёмся к теме, как решить, какой из подходов уместен в каждом случае.