Оператор ? и 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, остальные разбираются в тренажёре.
- Можно ли применять ? в функции, возвращающей Option?A)Да: None приводит к раннему возврату NoneB)Да, но только после преобразования Option в ResultC)Нет: ? работает только с ResultD)Да, и None при этом превращается в Err с пустой причиной
показать ответ и разбор
+A)Да: None приводит к раннему возврату None// разбор: Оператор определён через трейт Try, и Option его реализует: v? даёт значение при Some и мгновенно возвращает None при None. Удобно в цепочках поиска — first()?, get(k)?, parse().ok()?. Смешивать типы нельзя: в Option-функции ? применяют к Option, в Result-функции — к Result.
- Что позволяет писать ? для разных источников ошибок в функции с Box<dyn Error>?A)Компилятор стирает тип ошибки и подставляет универсальную обёрткуB)Специальное правило оператора ? для трейт-объектовC)Сплошная реализация From: ошибка оборачивается в BoxD)Автоматическая реализация Error для всех типов в крейте
показать ответ и разбор
+C)Сплошная реализация From: ошибка оборачивается в Box// разбор: В стандартной библиотеке есть impl<E: Error + 'static> From<E> for Box<dyn Error>, поэтому ? заворачивает любую подходящую ошибку в трейт-объект. Это быстрый способ склеить разнородные ошибки в приложении. Плата — вызывающий видит только Display и source, разобрать конкретный тип можно лишь через downcast.
- Что произойдёт при ? внутри main, объявленной как fn main() -> Result<(), Box<dyn Error>>?A)Ошибка молча игнорируется, код возврата остаётся нулевымB)Программа паникует с сообщением об ошибке и стеком вызововC)Компилятор не разрешит объявить main с типом ResultD)Программа напечатает ошибку и вернёт ненулевой код
показать ответ и разбор
+D)Программа напечатает ошибку и вернёт ненулевой код// разбор: Такой main разрешён с 2018-й редакции: рантайм печатает Error: {:?} в stderr и выставляет ненулевой код возврата. Из-за Debug вывод выглядит сыровато (Os { code: 2, kind: NotFound }), поэтому в CLI-утилитах ошибку обычно ловят руками и печатают через Display, а код возврата задают явно.
- Оператор ? делает ранний возврат. Что происходит с уже созданными в функции значениями?A)Они перемещаются в возвращаемую ошибку для последующей очисткиB)Для них вызываются деструкторы, как при обычном возвратеC)Их освобождает вызывающая функция, когда разберёт ErrD)Они остаются в памяти до конца программы: ранний выход пропускает Drop
показать ответ и разбор
+B)Для них вызываются деструкторы, как при обычном возврате// разбор: Ранний выход через ? — обычный возврат из функции: компилятор вставляет вызовы drop для всех живых локальных значений в обратном порядке. Поэтому файл закроется, блокировка снимется, соединение вернётся в пул. Это и делает RAII устойчивым к путям с ошибками — забыть освобождение на «плохой ветке» невозможно.
- Где можно писать оператор ?A)В функции с типом возврата i32: компилятор подберёт подходящую обёртку самB)В функциях, возвращающих Result, Option или другой тип с реализацией TryC)Только в функциях, возвращающих Result с ошибкой Box<dyn Error>D)Везде, кроме замыканий и тел асинхронных функций и блоков
показать ответ и разбор
+B)В функциях, возвращающих Result, Option или другой тип с реализацией Try// разбор: Оператор разворачивается в ранний возврат, поэтому тип возврата функции обязан его допускать: Result, Option или свой тип с реализацией Try. В обычной функции с возвратом i32 компилятор скажет, что ? тут неприменим. В замыканиях и async-блоках оператор работает, но соотносится с их собственным типом результата, а не с типом внешней функции — это частый источник путаницы.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.