сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Ошибки в Rust

Оператор ? и From в Rust

Оператор ?

Главный инструмент повседневной работы с ошибками. Спрашивают, что он делает, почему иногда не компилируется, работает ли с Option и что происходит с локальными значениями при раннем выходе.

Стержень: ? разворачивает успех и делает ранний возврат при ошибке, приводя её тип через From.

// Формулировки: «что делает ??», «почему ? couldn't convert the error?», «работает ли ? с Option?»

Ранний выход без лестницы

let s = read(path)? означает: при Ok взять значение, при Err немедленно вернуть эту ошибку из текущей функции. Благодаря этому счастливый путь читается сверху вниз, а обработка не расползается лесенкой из match.

Работает оператор и с Option: в функции, возвращающей Option, v.first()? отдаёт значение при Some и мгновенно возвращает None. Смешивать типы в одной функции нельзя - какой у неё возврат, с тем ? и работает.

// Внутри это трейт Try, поэтому оператор поддерживают оба типа и некоторые пользовательские обёртки.

fn load(p: &str) -> Result<Config, Error> {
    let text = fs::read_to_string(p)?;
    let cfg = toml::from_str(&text)?;
    Ok(cfg)
}
ранний возврат
выход из функции сразу при ошибке
трейт Try
механизм, на котором работает оператор ?

Конверсия через From

Оператор приводит ошибку к типу возврата функции через From. Поэтому в функции с ParseIntError нельзя просто написать ? на чтении файла: реализации From<io::Error> for ParseIntError не существует, и компилятор скажет «? couldn't convert the error».

Отсюда три рабочих решения. Свой enum ошибок с реализациями From на каждый источник - так делают библиотеки. Box<dyn Error> - в стандартной библиотеке есть сплошная реализация, и туда заворачивается любая ошибка. anyhow - то же самое для приложений, только с контекстом и удобствами.

// Проверено на компиляторе: без нужной From ошибка именно E0277, и текст прямо указывает на невозможность преобразования.

From для ошибок
конверсия, которую применяет оператор ?
E0277
нет подходящей реализации трейта для преобразования

main с Result и деструкторы

main может возвращать Result<(), Box<dyn Error>>: при Err рантайм печатает ошибку через Debug и завершает процесс ненулевым кодом. Удобно для утилит, но вывод выглядит сыровато - Os { code: 2, kind: NotFound }. Для пользовательского CLI ошибку обычно ловят руками и печатают через Display.

Важная деталь про ранний выход: ? это обычный возврат из функции, поэтому деструкторы всех живых локальных значений отрабатывают в обратном порядке. Файл закроется, блокировка снимется, соединение вернётся в пул. Именно поэтому в Rust не нужен блок finally.

Result в main
возврат ошибки из main: печать через Debug и ненулевой код
RAII
resource acquisition is initialization: ресурс живёт ровно столько, сколько владеющая им переменная, поэтому деструкторы отрабатывают и на ветке с ошибкой

Как отвечать: «Что делает оператор ? и почему он иногда не компилируется?»

Он разворачивает Ok и при Err немедленно возвращает ошибку из текущей функции, поэтому основной поток кода остаётся линейным. Работает и с Option - там ранний возврат даёт None. Не компилируется он обычно по одной причине: тип ошибки не приводится к типу возврата. Оператор делает конверсию через From, и если реализации нет, компилятор скажет, что не может преобразовать ошибку. Лечу это либо своим enum ошибок с From на каждый источник, либо Box<dyn Error> и anyhow в прикладном коде. И при раннем выходе деструкторы локальных значений отрабатывают как при обычном возврате, так что ресурсы не текут.

Ты закрываешь и механику, и самую частую ошибку компиляции, и вопрос про ресурсы, который обычно идёт следом.

На чём валятся

  • Считают, что ? паникует при ошибке. Он возвращает её вызывающему.
  • Смешивают ? для Option и Result в одной функции.
  • Не знают про конверсию через From и не понимают ошибку E0277.
  • Думают, что ранний выход пропускает деструкторы - он ведёт себя как обычный возврат.
  • Оставляют Debug-вывод ошибки из main пользователю утилиты.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_question_op1 / 5
    Можно ли применять ? в функции, возвращающей Option?
    A)Да: None приводит к раннему возврату None
    B)Да, но только после преобразования Option в Result
    C)Нет: ? работает только с Result
    D)Да, и None при этом превращается в Err с пустой причиной
    показать ответ и разбор
    +A)Да: None приводит к раннему возврату None

    // разбор: Оператор определён через трейт Try, и Option его реализует: v? даёт значение при Some и мгновенно возвращает None при None. Удобно в цепочках поиска — first()?, get(k)?, parse().ok()?. Смешивать типы нельзя: в Option-функции ? применяют к Option, в Result-функции — к Result.

  2. #rs_question_op2 / 5
    Что позволяет писать ? для разных источников ошибок в функции с Box<dyn Error>?
    A)Компилятор стирает тип ошибки и подставляет универсальную обёртку
    B)Специальное правило оператора ? для трейт-объектов
    C)Сплошная реализация From: ошибка оборачивается в Box
    D)Автоматическая реализация Error для всех типов в крейте
    показать ответ и разбор
    +C)Сплошная реализация From: ошибка оборачивается в Box

    // разбор: В стандартной библиотеке есть impl<E: Error + 'static> From<E> for Box<dyn Error>, поэтому ? заворачивает любую подходящую ошибку в трейт-объект. Это быстрый способ склеить разнородные ошибки в приложении. Плата — вызывающий видит только Display и source, разобрать конкретный тип можно лишь через downcast.

  3. #rs_question_op3 / 5
    Что произойдёт при ? внутри main, объявленной как fn main() -> Result<(), Box<dyn Error>>?
    A)Ошибка молча игнорируется, код возврата остаётся нулевым
    B)Программа паникует с сообщением об ошибке и стеком вызовов
    C)Компилятор не разрешит объявить main с типом Result
    D)Программа напечатает ошибку и вернёт ненулевой код
    показать ответ и разбор
    +D)Программа напечатает ошибку и вернёт ненулевой код

    // разбор: Такой main разрешён с 2018-й редакции: рантайм печатает Error: {:?} в stderr и выставляет ненулевой код возврата. Из-за Debug вывод выглядит сыровато (Os { code: 2, kind: NotFound }), поэтому в CLI-утилитах ошибку обычно ловят руками и печатают через Display, а код возврата задают явно.

  4. #rs_question_op4 / 5
    Оператор ? делает ранний возврат. Что происходит с уже созданными в функции значениями?
    A)Они перемещаются в возвращаемую ошибку для последующей очистки
    B)Для них вызываются деструкторы, как при обычном возврате
    C)Их освобождает вызывающая функция, когда разберёт Err
    D)Они остаются в памяти до конца программы: ранний выход пропускает Drop
    показать ответ и разбор
    +B)Для них вызываются деструкторы, как при обычном возврате

    // разбор: Ранний выход через ? — обычный возврат из функции: компилятор вставляет вызовы drop для всех живых локальных значений в обратном порядке. Поэтому файл закроется, блокировка снимется, соединение вернётся в пул. Это и делает RAII устойчивым к путям с ошибками — забыть освобождение на «плохой ветке» невозможно.

  5. #rs_question_op5 / 5
    Где можно писать оператор ?
    A)В функции с типом возврата i32: компилятор подберёт подходящую обёртку сам
    B)В функциях, возвращающих Result, Option или другой тип с реализацией Try
    C)Только в функциях, возвращающих Result с ошибкой Box<dyn Error>
    D)Везде, кроме замыканий и тел асинхронных функций и блоков
    показать ответ и разбор
    +B)В функциях, возвращающих Result, Option или другой тип с реализацией Try

    // разбор: Оператор разворачивается в ранний возврат, поэтому тип возврата функции обязан его допускать: Result, Option или свой тип с реализацией Try. В обычной функции с возвратом i32 компилятор скажет, что ? тут неприменим. В замыканиях и async-блоках оператор работает, но соотносится с их собственным типом результата, а не с типом внешней функции — это частый источник путаницы.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.