Result и Option в Rust
Обработка ошибок в 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, остальные разбираются в тренажёре.
- Что произойдёт, если результат функции, возвращающей Result, просто не использовать?A)Компилятор выдаст предупреждение must_use, сборка пройдётB)Ничего: Result без обработки просто отбрасываетсяC)Программа завершится с ошибкой при выполнении этой строкиD)Сборка упадёт: игнорировать Result запрещено
показать ответ и разбор
+A)Компилятор выдаст предупреждение must_use, сборка пройдёт// разбор: Result помечен #[must_use], поэтому отброшенное значение даёт предупреждение unused_must_use («unused
Resultthat must be used») — код соберётся, но след в выводе останется. В проектах это часто поднимают до ошибки через #![deny(unused_must_use)]. Осознанный пропуск пишут явно: let _ = f(); — так видно, что решение принято, а не забыто. - Что делает ok_or("пусто") у Option?A)Печатает сообщение и завершает программу при NoneB)Превращает None в Err("пусто"), Some — в OkC)Оборачивает Option в Result, сохраняя None как Ok(None)D)Возвращает значение или строку по умолчанию
показать ответ и разбор
+B)Превращает None в Err("пусто"), Some — в Ok// разбор: Это переход от «значения нет» к «операция не удалась, вот причина»: типовой шаг, когда пустой Option нужно поднять выше по стеку через ?. Аргумент вычисляется сразу, поэтому для дорогой ошибки берут ok_or_else с замыканием. Обратный переход — метод ok(), он превращает Err в None.
- Чем map_err полезен при пробросе ошибки наверх?A)Заменяет Err на значение по умолчаниюB)Логирует ошибку и передаёт её дальше без измененийC)Повторяет операцию, если она вернула ошибкуD)Меняет тип ошибки, не трогая успешное значение
показать ответ и разбор
+D)Меняет тип ошибки, не трогая успешное значение// разбор: map_err применяет функцию к ветке Err: так низкоуровневую ошибку заворачивают в свой тип или обогащают контекстом перед возвратом. Ветка Ok проходит нетронутой. Когда преобразование выражено через From, map_err не нужен вовсе — конверсию делает оператор ?.
- Функция возвращает Result<Option<User>, DbError>. Что это моделирует?A)Запрос мог упасть, а мог отработать и не найти пользователяB)Ошибку и пустоту как одно и то же состояниеC)Запрос успешен, Option лишь помечает пустой результатD)Асинхронный запрос, результат которого ещё не готов
показать ответ и разбор
+A)Запрос мог упасть, а мог отработать и не найти пользователя// разбор: Два разных исхода не стоит сваливать в один: сломанное соединение — это ошибка, а отсутствие строки в таблице — обычный ответ. Ok(None) даёт вызывающему выбрать реакцию: показать пустой экран или ретраить. Если сплющить в Result<User, DbError>, «не найдено» станет ошибкой и потеряется в общем обработчике.
- Обходим 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 в пару векторов): успешные результаты уходят в дело, список сбоев — в отчёт, и видно, что именно не удалось.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.