Введение: философия обработки ошибок в Rust
В отличие от многих языков с исключениями (Java, Python, C++), Rust не использует классическую конструкцию try/catch. Вместо этого язык предлагает более явный и типобезопасный подход. Ошибки в Rust — это не скрытые потоки управления, а возвращаемые значения. Это делает код предсказуемым и защищает от «тихих» сбоев.
В Rust есть два основных типа для представления ошибочных ситуаций:
- Result<T, E> — для восстанавливаемых ошибок (ошибки ввода-вывода, парсинга, бизнес-логики).
- Option<T> — для отсутствия значения (аналог null, но безопасный).
В этой статье мы разберём, как работать с этими типами, чем их заменить из мира try/catch, и как писать надёжный код на Rust.
1. Result<T, E>: главный инструмент обработки ошибок
Тип Result — это enum с двумя вариантами: Ok(T) (успех) и Err(E) (ошибка). Он используется в большинстве функций, которые могут завершиться неудачей.
1.1. Создание и возврат Result
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?; // оператор ? — аналог try!
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Здесь оператор ? — это ключевой элемент. Он работает как try в других языках: если результат Err, функция немедленно возвращает эту ошибку. Если Ok — извлекает значение.
1.2. Обработка с помощью match
Самый явный способ — использовать match:
let result = read_file("config.txt");
match result {
Ok(content) => println!("Файл прочитан: {}", content),
Err(e) => eprintln!("Ошибка: {}", e),
}
Это аналог try/catch, но более гибкий и контролируемый.
1.3. Вспомогательные методы
.unwrap()— паникует при ошибке (только для прототипов)..expect(msg)— паникует с кастомным сообщением..unwrap_or(default)— возвращает значение по умолчанию..is_ok()/.is_err()— проверка состояния.
2. Option<T>: безопасная работа с отсутствием значения
Option — это ещё один enum с вариантами Some(T) и None. Он заменяет null и nullptr, исключая ошибки разыменования нулевого указателя.
2.1. Пример использования
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some("Alice".to_string())
} else {
None
}
}
let user = find_user(2);
println!("Пользователь: {}", user.unwrap_or("Гость".to_string()));
2.2. Комбинация с Result
Часто Option преобразуется в Result для единообразной обработки:
fn get_user_name(id: u32) -> Result<String, String> {
find_user(id).ok_or("Пользователь не найден".to_string())
}
Метод .ok_or() превращает Some в Ok, а None — в Err.
3. Оператор ?: элегантная замена try/catch
Оператор ? — это синтаксический сахар, который автоматически распаковывает Result или Option. Он доступен только в функциях, возвращающих соответствующий тип.
3.1. Пример с цепочкой вызовов
use std::num::ParseIntError;
fn parse_and_add(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x: i32 = a.parse()?;
let y: i32 = b.parse()?;
Ok(x + y)
}
Без ? пришлось бы писать громоздкий match для каждого вызова. Оператор ? делает код таким же чистым, как try/catch, но с явными типами.
3.2. Работа с Option
Оператор ? работает и с Option:
fn get_first_char(s: Option<&str>) -> Option<char> {
let s = s?; // если None — выходим
s.chars().next()
}
4. Паника: когда ошибка фатальна
В Rust есть механизм паники (panic!) — это аварийное завершение с размоткой стека. Он используется для невосстанавливаемых ошибок (например, выход за границы массива).
Лучшая практика: паника — для багов и инвариантов, Result — для внешних ошибок.
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Деление на ноль!");
}
a / b
}
В production-коде такие функции стоит заменять на Result.
5. Пользовательские типы ошибок
Для сложных проектов создают собственные типы ошибок. Это улучшает читаемость и позволяет добавлять контекст.
use std::fmt;
#[derive(Debug)]
enum MyError {
IoError(std::io::Error),
ParseError(String),
NotFound,
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::IoError(e) => write!(f, "Ошибка ввода-вывода: {}", e),
MyError::ParseError(s) => write!(f, "Ошиб