Error Handling
Внешние ссылки:
- https://youtu.be/f82wn-1DPas
- https://blog.logrocket.com/error-handling-rust/
- https://rust.nizeclub.ru/_11.html
Option
Option — это перечисление (enum), которое используется, когда значение может быть либо “чем-то” (Some), либо “ничем” (None). Это замена привычным null или nil из других языков, но с важным отличием: в Rust вы обязаны явно обработать возможность отсутствия значения. Определение Option в стандартной библиотеке:
enum Option<T> {
Some(T),
None,
}где:
T— это любой тип данных (например,i32,Stringи т.д.).Some(T)— значение есть, и оно равноT.None— значения нет.
Result
Result — это перечисление, которое используется для операций, которые могут завершиться успехом (Ok) или неудачей (Err):
enum Result<T, E> {
Ok(T), // Успех с результатом типа T
Err(E), // Ошибка с типом E
}где:
T— тип возвращаемого значения при успехе.E— тип ошибки при неудаче.
Обработка Option и Result
- Использовать
matchи обработать все варианты поведения; unwrap— “дай мне значение или паника” (еслиNoneилиErr, программа крашится);unwrap_or— “дай мне значение или что-то другое” (еслиNoneилиErr, возвращает запасное значение, которое ты указал);expect— “дай мне значение или паника с моим текстом” (какunwrap, но с твоим сообщением при краше);is_some— “скажи, есть ли там значение?” (возвращаетtrue, еслиSomeвOption, иfalse, еслиNone);is_none— “скажи, пусто ли там?” (возвращаетtrue, еслиNoneвOption, иfalse, еслиSome). Если коротко:unwrap,unwrap_orиexpectпытаются достать значение и что-то с ним сделать, аis_someиis_noneпросто проверяют, что внутри, не трогая само значение.
Оператор ‘?’
Оператор ? используется в функциях, возвращающих Result. Он автоматически возвращает Err из функции, если результат — Err, или извлекает значение из Ok:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Деление на ноль!"))
} else {
Ok(a / b)
}
}
fn safe_division(a: i32, b: i32) -> Result<i32, String> {
let result = divide(a, b)?;
Ok(result * 2) // Удваиваем результат
}
fn main() {
println!("{:?}", safe_division(10, 0)); // Err("Деление на ноль!")
println!("{:?}", safe_division(10, 2)); // Ok(10)
}Ошибки без восстановления
Ряд ошибок приводит к вылету приложения. Также можно вручную вызвать вылет командой panic!:
fn main() {
panic!("Battery critically low! Shutting down to prevent data loss.");
}При этом некоторое время тратится на закрытие приложения, очистку стека и данных. Можно переложить это на ОС, введя настройку в Cargo.toml:
[profile.release]
panic = 'abort'Пользовательские ошибки с enum
Иногда стандартных типов ошибок (например, String) недостаточно. Вы можете создать свои собственные ошибки с помощью enum:
enum MathError { // введём свой enum с типами ошибок
DivisionByZero,
NegativeNumber,
}
fn divide_with_custom_error(a: i32, b: i32) -> Result<i32, MathError> {
if b == 0 {
Err(MathError::DivisionByZero)
} else if a < 0 || b < 0 {
Err(MathError::NegativeNumber)
} else {
Ok(a / b)
} }
fn main() {
match divide_with_custom_error(10, 0) {
Ok(value) => println!("Результат: {}", value),
Err(MathError::DivisionByZero) => println!("Ошибка: деление на ноль"),
Err(MathError::NegativeNumber) => println!("Ошибка: отрицательное число"),
} }Преимущества:
- Чёткое определение всех возможных ошибок.
- Легко расширять (добавьте новый вариант в
enum).
Стратегии работы с ошибками
Подготовка примера
Допустим, мы берём вектор из строк-чисел, складываем их и возвращаем сумму как строку:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s); // to_int = заготовка, см. ниже реализацию
}
return accum.to_string();
}
fn main() {
let v = vec![String::from("3"), String::from("4")]; // Правильный ввод
let total = sum_str_vec(v);
println!("Total equals: {:?}", total);
let v = vec![String::from("3"), String::from("abc")]; // Неправильный ввод
let total = sum_str_vec(v);
println!("Total equals: {:?}", total);
}Для конвертации строки в числа, нужно реализовать функцию to_int в соответствии со стратегиями обработки ошибочного ввода. Конвертацию мы делаем функцией parse(), которая возвращает тип Result<T,E>, где T - значение, E - код ошибки.
Стратегия 1 - паника
В случае неверного ввода, программа полностью останавливается в панике. Метод unwrap() у типа Result<T,E> убирает проверки на ошибки и есть договор с компилятором о том, что ошибки в этом месте быть не может. Если она есть, программа падает с паникой:
fn to_int(s: &str) -> i32 {
s.parse().unwrap() }
Стратегия 2 - паника с указанием причины
В случае неверного ввода, программа сообщает фразу, заданную автором, далее полностью останавливается в панике. Метод expect() аналогичен unwrap(), но выводит сообщение:
fn to_int(s: &str) -> i32 {
s.parse().expect("Error converting from string") }
Стратегия 3 - обработать то, что возможно обработать
Можно сконвертировать и прибавить к результату те строки, которые позволяют это сделать, проигнорировать остальные. Метод unwrap_or() позволяет указать возвращаемое значение в случае ошибки:
fn to_int(s: &str) -> i32 {
s.parse().unwrap_or(0) } // при вводе "abc" вернётся 0, сумма = "3"
Более предпочтительный вариант использовать закрытие unwrap_or_else(), так как метод unwrap_or() будет вызван ДО того как будет отработана основная команда, ВНЕ ЗАВИСИМОСТИ от того, будет ли её результат Some или None. Это потеря производительности, а также потенциальные глюки при использовании внутри unwrap_or() сложных выражений. Закрытие unwrap_or_else() будет вызвано только в случае None, иначе же эта ветка не обрабатывается:
fn to_int(s: &str) -> i32 {
s.parse().unwrap_or_else(|_| 0) }Стратегия 4 - решение принимает вызывающая функция
Вместо возврата числа, возвращаем тип Option<число> - в зависимости от успешности функции, в нём будет либо само число, либо None:
fn to_int(s: &str) -> Option<i32> {
s.parse().ok() // ok() конвертирует Result<T,E> в Option<T>
И тогда вызывающая функция должна обработать результат:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
accum += match to_int(&s) {
Some(v) => v,
None => {
println!("Error converting a value, skipped");
0 // вернётся 0 +в лог пойдёт запись о пропуске
}, } }
return accum.to_string();
}Более короткий вариант записи через if let:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
if let Some(val) = to_int(&s) {
accum += val;
} else { println!("Error converting a value, skipped"); }
}
return accum.to_string();
}Тип Option<T> также имеет метод unwrap_or(), отсюда ещё вариант записи:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s).unwrap_or(0); // раскрываем Option<T>
}
return accum.to_string();
}Стратегия 5 - в случае проблем, передать всё в основную программу
Вместо передачи значения из функции, в случае каких-либо проблем, мы возвращаем None:
fn sum_str_vec (strs: Vec<String>) -> Option<String> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s)?; // в случае None, ? передаёт его далее на выход
}
Some(accum.to_string()) // на выход пойдёт значение или None
}Стратегия 6 - передать всё в основную программу с объяснением ошибки
Мы возвращаем проблему в основную программу с объясением проблемы. Для этого заводим структуру под ошибку, и передаём уже не объект Option<T>, а объект Result<T,E>, где E = SummationError. Для такого объекта есть метод ok_or(), который либо передаёт значение, либо передаёт ошибку нужного типа:
#[derive(Debug)]
struct SummationError;
fn sum_str_vec (strs: Vec<String>) -> Result<String, SummationError> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s).ok_or(SummationError)?;
}
Ok(accum.to_string())
}Вместо выдумывать свой собственный тип и конвертировать вывод метода parse() из Result<T,E> в Option<T>, а потом обратно, можно сразу протащить ошибку в объекте Result<T,E> в главную программу:
use std::num::ParseIntError; // тип ошибки берём из библиотеки
fn to_int(s: &str) -> Result<i32, ParseIntError> {
s.parse() // parse возвращает просто Result<T,E>
}
fn sum_str_vec (strs: Vec<String>) -> Result<String, ParseIntError> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s)?; } // ? передаёт ошибку нужного типа далее
Ok(accum.to_string())
}Однако, мы хотим скрыть подробности работы и ошибки от главной программы и передать ей ошибку в понятном виде, без разъяснения деталей её возникновения. Для этого можно сделать трансляцию ошибки из библиотечной в собственный тип, и далее передать методом map_err():
use std::num::ParseIntError;
#[derive(Debug)]
struct SummationError;
fn to_int(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
fn sum_str_vec (strs: Vec<String>) -> Result<String, SummationError> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s).map_err(|_| SummationError)?; // конвертация ошибки
} // перед передачей
Ok(accum.to_string())
}Где можно использовать оператор ‘?’
Оператор ? можно использовать только в функциях для возврата совместимых значений типа Result<T,E>, Option<T> или иных данных со свойством FromResidual. Для работы такого возврата в заголовке функции должен быть прописан возврат нужного типа данных.
При использовании ? на выражении типа Result<T,E> или Option<T>, ошибка Err(e) или None будет возвращена рано из функции, а в случае успеха - выражение вернёт результат, и функция продолжит работу.
Пример функции, которая возвращает последний символ 1ой строки текста:
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
} // lines() возвращает итератор на текст
// next() берёт первую строку текста. Если текст пустой - сразу возвращаем None
Дополнительные библиотеки работы с ошибками
- Anyhow - статья
- thiserror
- color-eyre