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

Result и Option в Rust

Result и Option

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

Стержень: Option отвечает «есть значение или нет», Result - «получилось или нет и почему», и оба обязаны быть разобраны.

// Формулировки: «Option или Result?», «что если не использовать Result?», «как собрать результаты, где часть упала?»

Разные вопросы - разные типы

Пустой список, ненайденный ключ, незаполненное поле это Option: значения просто нет, и ничего страшного не произошло. Диск не читается, строка не разбирается, соединение отвалилось это Result: операция не удалась, и есть причина.

Между ними ходят двумя методами: ok_or превращает None в Err с указанной причиной, ok() превращает Err в None, выбрасывая детали. Первый нужен, когда пустоту надо поднять наверх через ?, второй - когда причина сбоя неинтересна.

// Тип Result<Option<T>, E> - не избыточность, а точность: запрос мог упасть, а мог отработать и никого не найти. Это разные исходы с разной реакцией, и схлопывать их в один не стоит.

Option
значение есть или его нет
Result
успех со значением или неудача с причиной

Отброшенный результат и преобразования

Оба типа помечены #[must_use], поэтому просто вызвать функцию и не посмотреть на результат не выйдет тихо - компилятор предупредит. В проектах это часто ужесточают до ошибки через deny(unused_must_use), а осознанный пропуск пишут явно: let _ = f().

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

must_use
атрибут, предупреждающий об отброшенном значении
map_err
преобразование только ветки ошибки

Пакетная обработка

collect::<Result<Vec<_>, _>>() выглядит удобно: коллекция Result-ов собирается в Result коллекции. Но он обрывается на первой ошибке - для валидации формы это то что нужно, а для обхода тысячи файлов уже нет: 999 успешных результатов потеряются.

Обратная крайность - filter_map(|r| r.ok()): сбои исчезают молча, и в отчёте не видно, что половина файлов не прочиталась. Для пакетов берут partition по is_ok или fold в пару векторов: результаты идут в работу, список сбоев - в лог и в ответ.

let (ok, err): (Vec<_>, Vec<_>) =
    results.into_iter().partition(|r| r.is_ok());
короткое замыкание
остановка на первой ошибке при сборе в Result
partition
разделение потока на успехи и сбои

Как отвечать: «Когда Option, а когда Result?»

Option - когда значения может не быть и это нормальный исход: пустая коллекция, ключ не найден, необязательное поле. Result - когда операция могла не получиться и у неудачи есть причина, которую вызывающему полезно знать: чтение файла, разбор строки, запрос в сеть. Между ними легко ходить: ok_or поднимает пустоту до ошибки, ok() опускает ошибку до пустоты. И вполне рабочая комбинация - Result<Option<T>, E>: запрос мог упасть, а мог отработать и ничего не найти, это разные исходы и обрабатывают их по-разному.

Ответ даёт критерий, а не определения, и упоминает комбинацию Result<Option<T>>, которую в реальном коде видно постоянно, а в ответах кандидатов - редко.

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

  • Делают «не найдено» вариантом ошибки и теряют разницу между сбоем и пустым результатом.
  • Собирают пакет через collect в Result и молча теряют успешные результаты после первой ошибки.
  • Глушат сбои через filter_map с ok() - в логе потом ничего не найти.
  • Не знают про must_use и считают, что отброшенный Result исчезает бесследно.
  • Пишут map_err там, где достаточно реализовать From и положиться на ?.

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

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

  1. #rs_result_option1 / 5
    Что произойдёт, если результат функции, возвращающей Result, просто не использовать?
    A)Компилятор выдаст предупреждение must_use, сборка пройдёт
    B)Ничего: Result без обработки просто отбрасывается
    C)Программа завершится с ошибкой при выполнении этой строки
    D)Сборка упадёт: игнорировать Result запрещено
    показать ответ и разбор
    +A)Компилятор выдаст предупреждение must_use, сборка пройдёт

    // разбор: Result помечен #[must_use], поэтому отброшенное значение даёт предупреждение unused_must_use («unused Result that must be used») — код соберётся, но след в выводе останется. В проектах это часто поднимают до ошибки через #![deny(unused_must_use)]. Осознанный пропуск пишут явно: let _ = f(); — так видно, что решение принято, а не забыто.

  2. #rs_result_option2 / 5
    Что делает ok_or("пусто") у Option?
    A)Печатает сообщение и завершает программу при None
    B)Превращает None в Err("пусто"), Some — в Ok
    C)Оборачивает Option в Result, сохраняя None как Ok(None)
    D)Возвращает значение или строку по умолчанию
    показать ответ и разбор
    +B)Превращает None в Err("пусто"), Some — в Ok

    // разбор: Это переход от «значения нет» к «операция не удалась, вот причина»: типовой шаг, когда пустой Option нужно поднять выше по стеку через ?. Аргумент вычисляется сразу, поэтому для дорогой ошибки берут ok_or_else с замыканием. Обратный переход — метод ok(), он превращает Err в None.

  3. #rs_result_option3 / 5
    Чем map_err полезен при пробросе ошибки наверх?
    A)Заменяет Err на значение по умолчанию
    B)Логирует ошибку и передаёт её дальше без изменений
    C)Повторяет операцию, если она вернула ошибку
    D)Меняет тип ошибки, не трогая успешное значение
    показать ответ и разбор
    +D)Меняет тип ошибки, не трогая успешное значение

    // разбор: map_err применяет функцию к ветке Err: так низкоуровневую ошибку заворачивают в свой тип или обогащают контекстом перед возвратом. Ветка Ok проходит нетронутой. Когда преобразование выражено через From, map_err не нужен вовсе — конверсию делает оператор ?.

  4. #rs_result_option4 / 5
    Функция возвращает Result<Option<User>, DbError>. Что это моделирует?
    A)Запрос мог упасть, а мог отработать и не найти пользователя
    B)Ошибку и пустоту как одно и то же состояние
    C)Запрос успешен, Option лишь помечает пустой результат
    D)Асинхронный запрос, результат которого ещё не готов
    показать ответ и разбор
    +A)Запрос мог упасть, а мог отработать и не найти пользователя

    // разбор: Два разных исхода не стоит сваливать в один: сломанное соединение — это ошибка, а отсутствие строки в таблице — обычный ответ. Ok(None) даёт вызывающему выбрать реакцию: показать пустой экран или ретраить. Если сплющить в Result<User, DbError>, «не найдено» станет ошибкой и потеряется в общем обработчике.

  5. #rs_result_option5 / 5
    Обходим 1000 файлов и хотим и результаты, и список сбоев. Что взять?
    A)filter_map(|r| r.ok()) — ошибки не нужны, их просто нет в выводе
    B)collect::<Result<Vec<_>, _>>() — он вернёт всё сразу
    C)partition по is_ok или сбор в два вектора вручную
    D)unwrap внутри map: сбойный файл всё равно означает баг
    показать ответ и разбор
    +C)partition по is_ok или сбор в два вектора вручную

    // разбор: collect в Result обрывается на первой ошибке — для пакетной обработки это не то: 999 успешных результатов потеряются. filter_map с ok() глушит сбои молча. Нужен partition (или fold в пару векторов): успешные результаты уходят в дело, список сбоев — в отчёт, и видно, что именно не удалось.

дальше

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

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