Что делает оператор вопроса (?) в Rust и как он работает с ошибками?
Оператор `?` — это сахар для быстрой обработки ошибок
Оператор вопроса ? в Rust — это синтаксический сахар, который заменяет стандартный шаблон match для обработки Result и Option. Его главная цель — сделать код чище и избежать вложенных конструкций при работе с ошибками.
Как это работает под капотом
Когда вы пишете File::open(path)?, компилятор преобразует это в примерно такой код:
let file = match File::open(path) {
Ok(value) => value, // если Ok — извлекаем значение
Err(e) => return Err(e.into()), // если Err — немедленно возвращаем ошибку из функции
};
Ключевые моменты:
- Извлечение значения: если результат
Ok(value), то?"распаковывает" его и присваивает переменной. - Ранний возврат: если результат
Err(e), то функция немедленно завершается и возвращает эту ошибку (с возможным преобразованием типа черезInto). - Тип ошибки: ошибка автоматически конвертируется в тип, который ожидает возвращаемое значение функции (благодаря трейту
From).
Сравнение с альтернативами
Давайте посмотрим на тот же код без ?:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut content = String::new();
match file.read_to_string(&mut content) {
Ok(_) => {},
Err(e) => return Err(e),
};
Ok(content)
}
Согласитесь, с ? код гораздо читаемее. А вот что делает unwrap():
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path).unwrap(); // паника при ошибке!
let mut content = String::new();
file.read_to_string(&mut content).unwrap(); // паника при ошибке!
Ok(content)
}
unwrap() при ошибке вызывает panic! — программа аварийно завершается. ? же передаёт ошибку наверх, где её можно обработать. Это принципиальная разница.
Где можно использовать ?
- Только внутри функций, которые возвращают
ResultилиOption. - В функциях, возвращающих
Option,?работает аналогично: приNone— возвратNone. - В main() можно использовать
?, если пометить её какfn main() -> Result<(), Box<dyn Error>>.
Пример с разными типами ошибок
use std::num::ParseIntError;
fn parse_and_add(a: &str, b: &str) -> Result<i32, ParseIntError> {
let a: i32 = a.parse()?; // ParseIntError автоматически возвращается
let b: i32 = b.parse()?;
Ok(a + b)
}
// А если нужно работать с разными ошибками:
fn complex_operation() -> Result<String, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("file.txt")?; // io::Error
let number: i32 = content.trim().parse()?; // ParseIntError
Ok(format!("Number is {}", number))
}
В последнем примере Box<dyn Error> позволяет использовать ? с разными типами ошибок, так как они все реализуют трейт Error.
Когда НЕ использовать ?
- Когда ошибка означает фатальный сбой и программу нужно остановить — тогда
unwrap()илиexpect(). - Когда нужно обработать ошибку локально (логирование, повтор попытки, fallback-значение) — тогда пишите
matchили используйте комбинаторыmap_err,or_else.
Таким образом, ? — это элегантный способ "пробросить" ошибку наверх, не загромождая код. Он делает Rust код одновременно безопасным и читаемым.
Оператор `?` и трейт `Try` — более глубокий взгляд
Первый ответ объяснил основы, но стоит добавить, что ? работает не только с Result и Option, а с любым типом, реализующим трейт Try (стабилизирован в Rust 1.65). Это даёт возможность использовать ? в более экзотических случаях, например, с Poll или ControlFlow.
Важный нюанс: ? выполняет автоматическое преобразование ошибок через Into. Это значит, что если ваша функция возвращает Result<T, MyError>, а внутренняя операция возвращает Result<T, OtherError>, то ? попытается сконвертировать OtherError в MyError через трейт From. Если такой конвертации нет — компилятор выдаст ошибку.
Пример с пользовательской ошибкой:
#[derive(Debug)]
enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
impl From<std::io::Error> for MyError {
fn from(e: std::io::Error) -> Self { MyError::Io(e) }
}
impl From<std::num::ParseIntError> for MyError {
fn from(e: std::num::ParseIntError) -> Self { MyError::Parse(e) }
}
fn process() -> Result<i32, MyError> {
let content = std::fs::read_to_string("file.txt")?; // io::Error -> MyError::Io
let num: i32 = content.trim().parse()?; // ParseIntError -> MyError::Parse
Ok(num)
}
Это позволяет строить чистые иерархии ошибок без лишнего кода. Рекомендую также изучить крейт thiserror для автоматической генерации таких преобразований — он отлично сочетается с оператором ?.