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