Error Handling

Внешние ссылки:

Option

Option — это перечисление (enum), которое используется, когда значение может быть либо “чем-то” (Some), либо “ничем” (None). Это замена привычным null или nil из других языков, но с важным отличием: в Rust вы обязаны явно обработать возможность отсутствия значения. Определение Option в стандартной библиотеке:

enum Option<T> {
    Some(T),
    None,
}

где:

  • T — это любой тип данных (например, i32String и т.д.).
  • 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). Если коротко: unwrapunwrap_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

Дополнительные библиотеки работы с ошибками