Что такое Result и Option в Rust и как с ними работать?
В Rust Result и Option — это два базовых перечисления (enums), с помощью которых язык реализует безопасную обработку ошибок и работу с отсутствующими значениями. Вместо привычных null, None, undefined, try/catch или исключений, Rust принудительно заставляет программиста обрабатывать каждую потенциальную ошибку или неопределённость.
Option<T>: значение либо есть, либо нет
Option<T> используется, когда значение может отсутствовать. Он определяется так:
enum Option<T> {
Some(T),
None,
}
-
Some(T) — обёртка над существующим значением.
-
None — означает отсутствие значения.
Пример:
fn get_first_element(vec: &Vec<i32>) -> Option<i32> {
vec.get(0).copied()
}
fn main() {
let numbers = vec!\[10, 20, 30\];
match get_first_element(&numbers) {
Some(n) => println!("Первый элемент: {}", n),
None => println!("Вектор пустой"),
}
}
Метод .get(index) у Vec возвращает Option<&T>, а .copied() делает из &T — T.
Почему это важно:
-
Rust не допускает null-ссылок.
-
Ошибка из-за null dereference — невозможна, если код компилируется.
-
Компилятор заставляет вас обработать None.
Удобные методы для работы с Option:
-
.is_some(), .is_none()
-
.unwrap(), .expect(msg)
-
.map(), .and_then(), .unwrap_or(), .unwrap_or_else()
-
if let Some(val) = ... и match
Result<T, E>: успешное значение или ошибка
Result<T, E> применяется для обработки ошибок. Он определён так:
enum Result<T, E> {
Ok(T),
Err(E),
}
-
Ok(T) — успешный результат.
-
Err(E) — ошибка.
Пример:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Деление на ноль".to_string())
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Результат: {}", result),
Err(e) => println!("Ошибка: {}", e),
}
}
Если использовать .unwrap() вместо match, то Err вызовет паник.
Преимущества Result:
-
В отличие от исключений, ошибки явные.
-
Типовая система обязывает обрабатывать Err.
-
Можно определить любой тип ошибки (String, io::Error, своё перечисление и т. д.)
Работа с Result и Option: идиомы Rust
1. ? оператор
Это синтаксический сахар, позволяющий пробрасывать ошибки вверх по стеку вызовов:
fn parse_number(input: &str) -> Result<i32, std::num::ParseIntError> {
let num = input.parse::<i32>()?;
Ok(num \* 2)
}
Если parse() вернёт Err, вся функция parse_number немедленно вернёт эту ошибку.
? работает как с Result, так и с Option:
fn get_first_char(s: &str) -> Option<char> {
let first = s.chars().next()?;
Some(first.to_ascii_uppercase())
}
Если .next() вернёт None, функция get_first_char вернёт None.
2. Комбинирование и трансформация
Вы можете использовать .map() и .and_then() для цепочек:
let maybe_len = Some("hello")
.map(|s| s.len());
let maybe_parsed = Some("42")
.and_then(|s| s.parse::<i32>().ok());
Для Result аналогично:
let parsed = "42"
.parse::<i32>()
.map(|n| n \* 2)
.unwrap_or(0);
Преобразование между Option и Result
Иногда нужно перейти от одного типа к другому:
let opt = Some(42);
let res: Result<i32, &str> = opt.ok_or("Нет значения");
let res2: Result<i32, String> = "42".parse().map_err(|e| e.to_string());
Методы:
-
.ok_or(err) превращает Option в Result
-
.ok_or_else(|| ...) — ленивый аналог
-
.ok() превращает Result в Option, теряя ошибку
Практические случаи использования
-
Option — когда отсутствие значения нормально и ожидаемо (например, элемент в коллекции, парсинг строки, поиск по ключу).
-
Result — когда может произойти ошибка, и вы хотите либо обработать её, либо пробросить дальше (например, работа с файлами, сетевыми запросами, преобразования типов).
Почему это важно в Rust
Главная идея в Rust: непрерывный контроль над ситуацией и предсказуемость исполнения программы. Option и Result обеспечивают:
-
Безопасность на уровне компиляции.
-
Гибкие способы обработки и пробрасывания ошибок.
-
Минимум runtime-накладных расходов (всё разворачивается в enum-ветвления, без try/catch).
Вместо ловушек вроде null, throw, panic, вы работаете с кодом, где всё возможное поведение явно указано в типах, и компилятор помогает вам написать корректную программу.