Avatar ·

Что означает оператор вопросительного знака (?) в Rust и как он работает?

Всем привет! Я начал изучать Rust недавно и наткнулся на странный оператор — вопросительный знак (?). Видел его в чужом коде, но не могу понять, что он делает. Вот пример, который я нашёл: ```rust use std::fs::File; use std::io::Read; fn read_file(path: &str) -> Result { let mut file = File::open(path)?; // Что здесь делает `?`? let mut contents = String::new(); file.read_to_string(&mut contents)?; // И здесь? Ok(contents) } ``` Я попробовал написать похожий код без этого оператора, используя `match`, и получилось очень громоздко: ```rust fn read_file_verbose(path: &str) -> Result { let mut file = match File::open(path) { Ok(f) => f, Err(e) => return Err(e), }; let mut contents = String::new(); match file.read_to_string(&mut contents) { Ok(_) => {}, Err(e) => return Err(e), }; Ok(contents) } ``` Получается, `?` заменяет целую конструкцию `match`? Но как именно? Может ли он работать не только с `Result`, но и с другими типами? И что будет, если я использую его в функции, которая возвращает не `Result`, а что-то другое? Объясните, пожалуйста, новичку. Заранее спасибо! <<>>

Что такое оператор `?` в Rust?

Оператор ? (вопросительный знак) — это синтаксический сахар для краткой обработки ошибок. Он предназначен для работы с типами Result<T, E> и Option<T>. Его основная задача — упростить код, избавляя вас от необходимости писать громоздкие конструкции match каждый раз, когда вы работаете с потенциально ошибочными операциями.

Как он работает?

Когда вы пишете выражение?, компилятор Rust делает следующее:

  • Если выражение возвращает Ok(value) (для Result) или Some(value) (для Option), то оператор ? извлекает значение и передаёт его дальше.
  • Если выражение возвращает Err(e) (для Result) или None (для Option), то оператор ? немедленно возвращает ошибку из текущей функции.

Важно: оператор ? можно использовать только в функциях, которые сами возвращают Result или Option (или другой тип, реализующий трейт FromResidual).

Пример с Result

Ваш код с match можно переписать так:

use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;  // Если ошибка — сразу return Err(...)
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;  // То же самое
    Ok(contents)
}

Здесь File::open(path)? — это эквивалент:

let file = match File::open(path) {
    Ok(f) => f,
    Err(e) => return Err(e.into()),  // Автоматическое преобразование ошибки!
};

Обратите внимание: ? автоматически вызывает From::from() для преобразования типа ошибки, если это необходимо. Например, если ваша функция возвращает Result<_, MyError>, а вложенная операция возвращает Result<_, std::io::Error>, то ? попытается преобразовать std::io::Error в MyError через трейт From.

Пример с Option

Оператор ? работает и с Option:

fn get_first_element(list: &[i32]) -> Option<&i32> {
    let first = list.first()?;  // Если список пуст — вернётся None
    Some(first)
}

Это эквивалентно:

fn get_first_element(list: &[i32]) -> Option<&i32> {
    let first = match list.first() {
        Some(v) => v,
        None => return None,
    };
    Some(first)
}

Цепочки вызовов

Оператор ? можно использовать в цепочках:

use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    fs::read_to_string("config.toml")?
        .lines()
        .next()
        .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "No lines"))?
        .to_string())
}

Здесь каждый ? проверяет результат предыдущего шага.

Ограничения

  • Нельзя использовать ? в функциях, которые не возвращают Result или Option (например, в fn main() без возвращаемого типа).
  • Тип ошибки должен быть совместим: либо совпадать, либо быть преобразуемым через From.

Итог

Оператор ? — это элегантный способ обработки ошибок без лишнего шума. Он делает код более читаемым и лаконичным, сохраняя при этом безопасность типов. Используйте его везде, где это возможно!

<<>>

Альтернативный взгляд: оператор `?` и трейт `Try`

На самом деле оператор ? не жёстко привязан к Result и Option. Он работает с любым типом, который реализует трейт Try (стабилизирован в Rust 1.65). Этот трейт определяет, как тип должен вести себя при успехе и неудаче.

Например, вы можете создать свой тип ошибки и реализовать для него Try, чтобы использовать ?:

use std::ops::Try;

enum MyResult<T> {
    Value(T),
    Error(String),
}

impl<T> Try for MyResult<T> {
    type Output = T;
    type Residual = String;

    fn from_output(output: T) -> Self {
        MyResult::Value(output)
    }

    fn branch(self) -> std::ops::ControlFlow<Self::Residual, T> {
        match self {
            MyResult::Value(v) => std::ops::ControlFlow::Continue(v),
            MyResult::Error(e) => std::ops::ControlFlow::Break(e),
        }
    }
}

После этого вы сможете писать:

fn foo() -> MyResult<i32> {
    let x = bar()?;  // Работает!
    MyResult::Value(x + 1)
}

Также стоит помнить, что ? может использоваться в асинхронных функциях (async fn) и в функциях с типами вроде Poll<Result<T, E>>. Это делает его универсальным инструментом для управления потоком выполнения.

Совет: если вы часто сталкиваетесь с разными типами ошибок, используйте крейт anyhow или thiserror — они упрощают преобразование ошибок и делают работу с ? ещё удобнее.