Error Handling
External link: https://youtu.be/f82wn-1DPas
Подготовка примера
Допустим, мы берём вектор из строк-чисел, складываем их и возвращаем сумму как строку:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s);
}
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>
также имеет метод unwarp_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