ПрограммированиеСистемный программист

Объясните, как происходит управление ошибками через Result<T, E> и как правильно использовать question mark operator (?), не нарушая safety и читаемость кода.

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

Ответ.

Rust построен на явном управлении ошибками: исключения отсутствуют, вместо этого используется возвращаемое значение Result<T, E>. Это обеспечивает безопасность и предсказуемость кода.

История вопроса:

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

Проблема:

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

Решение:

Ключевой инструмент — тип Result<T, E> и оператор ?, автоматически "разворачивающий" результат: при ошибке происходит немедленный выход из функции с возвратом ошибки, а при успехе возвращается значение.

Пример кода:

fn read_number(file: &str) -> Result<i32, std::io::Error> { let content = std::fs::read_to_string(file)?; let num: i32 = content.trim().parse()?; Ok(num) }

Ключевые особенности:

  • Явная декларируемость всех потенциальных ошибок в сигнатуре
  • Оператор ? позволяет упрощать вложенность (убирает if-let/unwrap/expect)
  • errors могут быть легко "обёрнуты" или преобразованы (через .map_err() и другие методы)

Вопросы с подвохом.

Можно ли использовать ? в функциях, возвращающих Unit (void)?

Нет, оператор ? допустим только внутри функций, возвращающих Result или Option. Если функция возвращает (), ? использовать нельзя.

Что произойдёт, если типы ошибок в нескольких вызовах с ? разные?

Получается ошибка компиляции: тип ошибки должен быть однозначно определён. Нужно либо привести все ошибки к единому типу через .map_err() или использовать thiserror, либо описывать enum-обёртку на уровне API. Пример:

fn foo() -> Result<_, MyError> { let a = bar()?; let b = baz().map_err(MyError::Baz)?; Ok() }

Чем опасен .unwrap() во внутренней логике?

Распространено ошибочное мнение, что в "основном" коде можно смело использовать unwrap(), "оно точно не упадёт". На деле даже маленькая невидимая ошибка приведёт к панике времени выполнения — нарушится безопасность программы.

Типовые ошибки и анти-паттерны

  • Перехват всех ошибок через unwrap/expect, особенно во внутренней логике
  • Потеря контекста при map_err(|_| …), когда ошибка замывается без сохранения исходной информации
  • Избыточная вложенность при ручной обработке каждой ошибки без ?

Пример из жизни

Негативный кейс

В продакшен-коде оставили много unwrap() после быстрой отладки. В итоге приложение падало при любом сбое парсинга из-за некорректного ввода пользователя.

Плюсы:

  • Быстрая отладка

Минусы:

  • Потенциальный краш приложения, сложно найти причину

Позитивный кейс

Использовали полный стек Result<T, E> с эксплицитным описанием ошибок и применяли только ? и .map_err(), сохраняя всю информацию о сбое.

Плюсы:

  • Лёгкость отладки, предсказуемое поведение

Минусы:

  • Немного больше "шумных" типов ошибок