Глава 11: Обработка ошибок в Rust

Содержание: Option: работа с отсутствием значения Result: обработка ошибок Паника: panic!, unwrap, expect Пользовательские ошибки с enum Примеры: обработка файлов с Result Упражнение: Написать функцию с обработкой ошибок Проверочное задание Заключение

Добро пожаловать в десятую лекцию нашего курса по Rust! Сегодня мы погрузимся в одну из ключевых тем языка программирования Rust — обработку ошибок. Rust предлагает мощные и безопасные инструменты для работы с ситуациями, когда что-то может пойти не так: от отсутствия значения до критических сбоев. Эта лекция будет подробной, самодостаточной и подойдёт как новичкам, так и тем, кто хочет углубить свои знания. Мы разберём всё шаг за шагом: от базовых концепций до практических примеров и упражнений.


Введение в обработку ошибок

В программировании ошибки неизбежны. Файл может не открыться, пользователь может ввести неверные данные, сеть может быть недоступна. Rust делает обработку ошибок явной и безопасной, избегая таких проблем, как "null pointer exceptions" в других языках.

"Null pointer exception" (исключение нулевого указателя) — это ошибка в программировании, которая возникает, когда программа пытается использовать объект или переменную, которая имеет значение null (то есть не указывает на реальный объект в памяти). Например, если вы попытаетесь вызвать метод у объекта, который не был инициализирован, компилятор или среда выполнения выдаст эту ошибку. Это распространённая проблема в языках вроде Java, C++ и других, где требуется явное управление памятью или ссылками.

Вместо этого Rust использует два основных типа для работы с ошибками:

  1. Option — для случаев, когда значение может отсутствовать.
  2. Result — для случаев, когда операция может завершиться неудачей.

Кроме того, мы рассмотрим ситуации, когда программа "паникует" (прерывается), и научимся создавать свои собственные ошибки. К концу лекции вы сможете уверенно обрабатывать ошибки в своих программах.


1. Option: работа с отсутствием значения

Что такое Option?

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

Определение Option в стандартной библиотеке выглядит так:

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

Пример использования Option

Представьте функцию, которая ищет элемент в массиве по индексу. Если индекс выходит за пределы массива, функция возвращает None:

fn get_element(arr: &[i32], index: usize) -> Option<i32> {
    if index < arr.len() {
        Some(arr[index])
    } else {
        None
    }
}

fn main() {
    let numbers = [1, 2, 3];
    let result = get_element(&numbers, 1);
    println!("Результат: {:?}", result); // Some(2)
    let result = get_element(&numbers, 5);
    println!("Результат: {:?}", result); // None
}

Здесь мы видим, что Option заставляет нас задуматься: а что делать, если элемента нет?

Обработка Option

Чтобы извлечь значение из Option, нужно обработать оба варианта: Some и None. Вот основные способы:

1. Использование match

fn main() {
    let numbers = [1, 2, 3];
    let result = get_element(&numbers, 1);
    match result {
        Some(value) => println!("Найдено значение: {}", value),
        None => println!("Значение не найдено"),
    }
}

match — это мощный инструмент, который мы подробно разберём в других лекциях. Он гарантирует, что вы обработаете все возможные случаи.

2. Метод unwrap (осторожно!)

Метод unwrap извлекает значение из Option или Result(например из Some), но вызывает панику (прерывание программы), если встречается None или Err

let value = get_element(&numbers, 1).unwrap(); // 2
let value = get_element(&numbers, 5).unwrap(); // Паника!

Используйте unwrap только если вы уверены, что значение есть.

3. Метод unwrap_or

Метод unwrap извлекает значение из Option или Result, а если там None или Err, возвращает запасное значение, которое ты указал, без паники.

Например, если вы хотите указать значение по умолчанию для случая None:

let value = get_element(&numbers, 5).unwrap_or(0); // 0

4. Метод is_some и is_none

Проверка состояния:

if get_element(&numbers, 1).is_some() {
    println!("Элемент найден!");
}

Итого простыми словами

Если коротко: unwrap, unwrap_or и expect пытаются достать значение и что-то с ним сделать, а is_some и is_none просто проверяют, что внутри, не трогая само значение.

Практический совет

Option учит вас думать о "пустых" случаях заранее. Это делает код надёжнее, чем в языках с null.


2. Result: обработка ошибок

Что такое Result?

Result — это ещё одно перечисление, которое используется для операций, которые могут завершиться успехом (Ok) или неудачей (Err):

enum Result<T, E> {
    Ok(T),  // Успех с результатом типа T
    Err(E), // Ошибка с типом E
}

Пример использования Result

Представьте функцию, которая делит два числа, но возвращает ошибку, если делитель равен нулю:

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Деление на ноль!"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    let result = divide(10, 2);
    println!("Результат: {:?}", result); // Ok(5)
    let result = divide(10, 0);
    println!("Результат: {:?}", result); // Err("Деление на ноль!")
}

Обработка Result

Как и с Option, вы должны явно обработать оба варианта.

1. Использование match

fn main() {
    match divide(10, 2) {
        Ok(value) => println!("Результат деления: {}", value),
        Err(error) => println!("Ошибка: {}", error),
    }
}

2. Метод unwrap и expect

let value = divide(10, 2).unwrap(); // 5
let value = divide(10, 0).expect("Не удалось разделить числа"); // Паника с сообщением

3. Метод unwrap_or

Значение по умолчанию при ошибке:

let value = divide(10, 0).unwrap_or(0); // 0

4. Оператор ?

Оператор ? используется в функциях, возвращающих Result. Он автоматически возвращает Err из функции, если результат — Err, или извлекает значение из Ok:

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)
}

Практический совет

Result — ваш лучший друг для обработки ошибок. Используйте ?, чтобы сократить код, но не злоупотребляйте unwrap.


3. Паника: panic!, unwrap, expect

Что такое паника?

Паника — это аварийное завершение программы, когда ошибка настолько серьёзна, что продолжение невозможно. В Rust панику можно вызвать вручную с помощью макроса panic!:

fn main() {
    panic!("Всё сломалось!");
}

При выполнении этого кода программа завершится с сообщением "Всё сломалось!".

Когда использовать panic!?

panic! подходит для случаев, которые не должны происходить в нормальной работе программы (например, баг в логике). Для ожидаемых ошибок используйте Result.

unwrap и expect как источники паники

Как мы видели, unwrap и expect вызывают панику, если OptionNone или ResultErr. Это удобно для быстрого прототипирования, но в продакшен-коде лучше заменять их на явную обработку.


4. Пользовательские ошибки с enum

Иногда стандартных типов ошибок (например, String) недостаточно. Вы можете создать свои собственные ошибки с помощью enum:

enum MathError {
    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!("Ошибка: отрицательное число"),
    }
}

Преимущества


5. Примеры: обработка файлов с Result

Давайте разберём реальный пример — чтение файла:

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file("example.txt") {
        Ok(contents) => println!("Содержимое файла: {}", contents),
        Err(e) => println!("Ошибка: {}", e),
    }
}

Если файла нет, программа выведет ошибку вроде "No such file or directory".


6. Упражнение: Написать функцию с обработкой ошибок

Задание

Напишите функцию parse_number, которая принимает строку и возвращает Result<i32, String>. Функция должна:

Решение

fn parse_number(input: &str) -> Result<i32, String> {
    if input.is_empty() {
        return Err(String::from("Строка пустая"));
    }
    match input.parse::<i32>() {
        Ok(number) => Ok(number),
        Err(_) => Err(String::from("Невозможно преобразовать в число")),
    }
}

fn main() {
    println!("{:?}", parse_number("42"));    // Ok(42)
    println!("{:?}", parse_number(""));      // Err("Строка пустая")
    println!("{:?}", parse_number("abc"));   // Err("Невозможно преобразовать в число")
}

Разбор


Проверочное задание

  1. Напишите функцию divide_and_add, которая принимает два числа, делит их с помощью divide (из примера выше), прибавляет 5 к результату и возвращает Result<i32, String>.
  2. Обработайте результат в main с помощью match.

Попробуйте сами, а затем проверьте с решением ниже:

Решение

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Деление на ноль!"))
    } else {
        Ok(a / b)
    }
}

fn divide_and_add(a: i32, b: i32) -> Result<i32, String> {
    let result = divide(a, b)?;
    Ok(result + 5)
}

fn main() {
    match divide_and_add(10, 2) {
        Ok(value) => println!("Результат: {}", value), // 10
        Err(e) => println!("Ошибка: {}", e),
    }
    match divide_and_add(10, 0) {
        Ok(value) => println!("Результат: {}", value),
        Err(e) => println!("Ошибка: {}", e), // "Деление на ноль!"
    }
}

Заключение

Обработка ошибок в Rust — это мощный инструмент, который делает ваш код безопасным и предсказуемым. Option и Result заставляют вас заранее думать о возможных проблемах, а оператор ? упрощает работу с цепочками операций. Паника — это крайний случай, а пользовательские ошибки дают гибкость.

Практикуйтесь, экспериментируйте с примерами и переходите к следующей лекции с уверенностью! Если что-то непонятно, перечитайте разделы или попробуйте изменить примеры кода. Удачи в изучении Rust!