Что именно делает оператор ? с результатом Err внутри функции, возвращающей Result?

Что именно делает оператор ? с результатом Err внутри функции, возвращающей Result?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Оператор ? немедленно завершает текущую функцию с той же ошибкой, если выражение имеет значение Err. При значении Ok он извлекает успешный результат и позволяет продолжить выполнение; сам по себе ? не вызывает panic.

Исторический контекст

В Rust ошибки выполнения обычно представлены как обычные значения типов Result и Option, а не как исключения, скрыто передающие управление между слоями программы. Такой подход делает возможность ошибки частью сигнатуры функции и позволяет компилятору проверять обработку результата.

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

Постановка проблемы

Без ? каждая потенциально ошибочная операция требует явно разобрать Ok и Err. В цепочке из нескольких операций это быстро увеличивает вложенность и затрудняет чтение основного сценария.

Неверное понимание оператора приводит к двум типичным ошибкам: ожиданию, что он обработает ошибку на месте, или ожиданию, что он вызовет panic. На самом деле он только передаёт ошибку вызывающему коду, поэтому на границе приложения всё равно должен существовать слой, который решит, как её показать, записать в журнал или преобразовать в ответ протокола.

Подробное решение

Для Result<T, E> оператор ? логически ведёт себя так: при Ok(value) извлекает value, а при Err(error) возвращает ошибку из текущей функции. Поэтому функция должна возвращать совместимый тип результата, например Result с тем же типом ошибки либо с типом, в который исходная ошибка может быть преобразована через From.

Минимальный пример:

use std::num::ParseIntError; fn parse_number(text: &str) -> Result<i32, ParseIntError> { let number = text.parse::<i32>()?; Ok(number + 1) }

Если разбор строки завершится ошибкой, тело функции после ? не продолжится, а вызывающий код получит Err. Если разбор успешен, в number попадёт само числовое значение, а не оболочка Result.

Для Option<T> оператор работает аналогично: Some(value) извлекается, а None немедленно возвращается из текущей функции. Поэтому тип возвращаемого значения должен соответствовать варианту: ? над Result требует функции, возвращающей Result, а ? над Option — функции, возвращающей Option.

Оператор не исправляет ошибку, не логирует её и не выбирает запасное значение. Если требуется локальная реакция, применяют сопоставление, преобразование результата или методы вроде map_err, or_else и unwrap_or; выбор зависит от того, является ли ошибка частью контракта вызывающего слоя или должна быть обработана немедленно.

Если тип ошибки функции отличается от типа ошибки операции, неявное преобразование через From позволяет использовать ?, но такое преобразование должно быть спроектировано осмысленно. Слишком широкая ошибка верхнего уровня упрощает маршрутизацию, но может скрыть важные причины; слишком детальная ошибка усложняет публичный контракт и связывает слои реализации.

Ситуация из практики

Сервис читает конфигурацию, разбирает её и затем подключается к базе данных. Внутри этих шагов возникают разные ошибки: ошибки ввода-вывода, синтаксиса и подключения.

Первый вариант — обрабатывать каждую ошибку вручную на каждом шаге. Он даёт полный контроль над сообщениями и действиями, но создаёт много повторяющегося кода и легко приводит к разным правилам обработки похожих ошибок.

Второй вариант — возвращать низкоуровневые ошибки напрямую. Это кратко, но внешний слой начинает зависеть от деталей библиотек и хуже отличает, например, повреждённую конфигурацию от временной недоступности базы данных.

Выбранный вариант — определить доменный тип ошибки сервиса и настроить преобразование низкоуровневых ошибок в него, после чего использовать ? внутри основной логики. В результате внутренний код остаётся линейным, а граница сервиса получает стабильный контракт: она может выбрать корректный ответ, журналирование и политику повторной попытки без раскрытия деталей реализации.

Что кандидаты часто упускают

  1. Меняет ли оператор ? исходный тип ошибки?

    Сам оператор не обязан оставлять тип ошибки неизменным. При передаче Err он может преобразовать ошибку в тип, ожидаемый текущей функцией, если доступно подходящее преобразование From. Это позволяет нижним слоям возвращать специализированные ошибки, а верхнему слою — собирать их в единый тип.

    Однако преобразование не должно быть формальным способом скрыть несовместимость контрактов. Если разные причины требуют разных действий, их следует сохранить различимыми в доменной модели ошибки.

  2. Можно ли считать ? обработкой ошибки?

    Нет. ? выполняет ранний возврат и делегирует решение вызывающему коду. Ошибка считается обработанной только тогда, когда какой-либо слой преобразовал её в понятное действие: восстановление, альтернативный результат, сообщение пользователю, запись в журнал или завершение операции.

    Поэтому функция, использующая ?, обычно не является конечной точкой обработки. Она лишь сохраняет ошибку и передаёт её выше, пока не будет достигнут слой с достаточным контекстом для принятия решения.

  3. Чем ? отличается от unwrap и expect по последствиям?

    ? сохраняет ошибку в типизированном результате и позволяет вызывающему коду решить, что делать дальше. unwrap и expect извлекают успешное значение, но при ошибке вызывают panic, поэтому подходят только там, где неуспех действительно нарушает доказанное внутреннее предположение или является намеренно фатальным.

    Использование unwrap вместо ? в обработке внешних данных превращает ожидаемую ошибку ввода, файла или сети в аварийное завершение. Это ухудшает надёжность и контроль над границей приложения; expect лишь добавляет диагностическое сообщение и не меняет сам факт аварийного завершения.