Введение: почему Rust не использует try/catch?
В большинстве языков программирования (JavaScript, Python, Java) обработка ошибок строится на механизме исключений и конструкции try/catch. Однако в Rust такой подход отсутствует. Вместо этого разработчики используют мощную систему типов, которая делает ошибки частью сигнатуры функции. Это не случайность, а продуманное архитектурное решение: Rust стремится к предсказуемости и безопасности на этапе компиляции, исключая скрытые падения программы.
В этой статье мы разберём:
- Почему Rust отказался от
try/catch; - Как работают основные инструменты:
Option,Result,unwrap(),expect(); - Что такое оператор
?(вопросительный знак); - Когда использовать
panic!и макросtry!(устаревший); - Современные практики обработки ошибок в Rust 2024.
1. Основы: Option и Result — два главных типа
Тип Option: когда значение может отсутствовать
Option<T> — это перечисление (enum), которое представляет либо значение (Some(T)), либо его отсутствие (None). Он идеально подходит для ситуаций, когда ошибка в классическом понимании не происходит, но результат может быть пустым.
fn найти_пользователя(id: u32) -> Option<String> {
let база = vec!["Alice".to_string(), "Bob".to_string()];
if id < база.len() {
Some(база[id].clone())
} else {
None
}
}
fn main() {
match найти_пользователя(1) {
Some(имя) => println!("Найден: {}", имя),
None => println!("Пользователь не найден"),
}
}
Тип Result: успех или ошибка
Result<T, E> — основной инструмент для обработки ошибок. Он также является enum: Ok(T) (успех) или Err(E) (ошибка).
use std::fs::File;
use std::io::Read;
fn прочитать_файл(путь: &str) -> Result<String, std::io::Error> {
let mut файл = File::open(путь)?;
let mut содержимое = String::new();
файл.read_to_string(&mut содержимое)?;
Ok(содержимое)
}
2. Оператор ? (вопросительный знак) — замена try/catch
Оператор ? — это основной способ проброса ошибок наверх в Rust. Он заменяет громоздкие match конструкции и делает код чистым. Если результат — Ok, значение извлекается и присваивается переменной. Если Err — функция немедленно возвращает эту ошибку.
use std::fs;
use std::io;
fn копировать_файл(источник: &str, назначение: &str) -> io::Result<()> {
let данные = fs::read(источник)?; // если ошибка — выход
fs::write(назначение, &данные)?; // если ошибка — выход
Ok(())
}
fn main() -> io::Result<()> {
копировать_файл("source.txt", "dest.txt")?;
println!("Файл скопирован успешно");
Ok(())
}
Важно: оператор ? работает только в функциях, которые возвращают Result, Option или типы, реализующие трейт FromResidual.
3. panic!, unwrap и expect — когда ошибка фатальна
Макрос panic!
panic!() вызывает аварийное завершение программы (stack unwinding). Используется только в ситуациях, когда продолжение выполнения невозможно или бессмысленно: ошибки в логике программы, нарушение инвариантов, тесты.
fn деление(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Деление на ноль!");
}
a / b
}
Методы unwrap() и expect()
unwrap() — быстрый способ получить значение из Option или Result. Если внутри None или Err — программа паникует. expect() делает то же самое, но позволяет задать собственное сообщение об ошибке.
fn main() {
let число: Option<i32> = Some(42);
println!("{}", число.unwrap()); // 42
let ошибка: Option<i32> = None;
// println!("{}", ошибка.unwrap()); // panic!
println!("{}", ошибка.expect("Ожидалось число, но получили None"));
}
Рекомендация: Используйте unwrap() только в прототипах, тестах или когда вы на 100% уверены, что ошибки не будет. В production-коде предпочтительнее expect() с осмысленным сообщением.
4. Макрос try! — устаревший, но встречающийся
В ранних версиях Rust (до 1.13) оператора ? не существовало. Вместо него использовался макрос try!. Сейчас он считается устаревшим, но вы всё ещё можете встретить его в старом коде.
// Старый стиль (не рекомендуется)
fn старая_функция() -> Result<(), std::io::Error> {
let f = try!(File::open("foo.txt"));
Ok(())
}
// Современный стиль
fn новая_функция() -> Result<(), std::io::Error> {
let f = File::open("foo.txt")?;
Ok(())
}
Если вы поддерживаете легаси-проект на Rust, замените try! на оператор ? — это улучшит читаемость и совместимость с современными идиомами.
5. Создание собственных типов ошибок
Для сложных проектов неудобно использовать стандартные типы ошибок. Rust позволяет создавать собственные типы, реализуя трейт std::error::Error.
use std::fmt;
#[derive(Debug)]
enum MyError {
NotFound,
PermissionDenied(String),
IoError(std::io::Error),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) ->