Вопросы по обработке ошибок в Rust на собеседовании
Исключений в языке нет, поэтому ошибка — обычное значение, и её обработка становится частью сигнатуры функции. Собес проверяет чувство границы: что считать багом и уронить паникой, а что вернуть вызывающему, и как при этом не превратить каждый вызов в лестницу из match.
Что спрашивают
- +Result и Option: чем «ошибка» отличается от «пусто», map и and_then, ok_or и преобразования между ними
- +Оператор ?: как разворачивается, почему требует From, что мешает применить его в main без Result
- +Свои ошибки: enum против строки, Display и Error, source и цепочка причин, что попадает в публичный API
- +panic: unwrap и expect, где паника уместна, unwinding против abort, чем ловится в тестах
- +Экосистема: thiserror для библиотек и anyhow для приложений, контекст и downcast до конкретного типа
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- panic, unwrap, expect12
- Result и Option12
- thiserror и anyhow12
- Оператор ? и From12
- Свои типы ошибок12
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Result и Option в Rust12 вопросов
- Оператор ? и From в Rust12 вопросов
- Свои типы ошибок в Rust12 вопросов
- panic, unwrap и expect в Rust12 вопросов
- thiserror и anyhow12 вопросов
Примеры вопросов с разбором
- Что должен реализовать свой тип ошибки, чтобы вести себя как ошибка стандартной библиотеки?A)Debug и Display, а поверх них трейт std::error::ErrorB)Clone и PartialEq, чтобы ошибку можно было сравниватьC)Только Display: остальное компилятор добавит самD)Serialize из serde для передачи по сети
показать ответ и разбор
+A)Debug и Display, а поверх них трейт std::error::Error// разбор: Трейт Error требует Debug и Display как супертрейты: первый нужен для {:?} и вывода из main, второй — для человекочитаемого сообщения. Сам Error добавляет source() — ссылку на нижележащую причину, из которой складывается цепочка. Всё остальное — по желанию задачи.
- Как делят зоны ответственности thiserror и anyhow?A)thiserror — типы ошибок библиотеки, anyhow — ошибка приложенияB)thiserror генерирует ошибки, anyhow их логируетC)thiserror — для синхронного кода, anyhow — для асинхронногоD)thiserror для ошибок ввода-вывода, anyhow для остальных
показать ответ и разбор
+A)thiserror — типы ошибок библиотеки, anyhow — ошибка приложения// разбор: Библиотека обязана дать пользователю разбираемый тип: thiserror генерирует Display, Error и From по атрибутам над enum. Приложению чаще нужен один тип на всё с контекстом — это anyhow::Error, он поглощает любую ошибку и умеет добавлять пояснения. Обратный порядок неудобен: anyhow в публичном API лишает пользователя возможности разобрать причину.
- Что делает unwrap() у Result::Err?A)Возвращает саму ошибку вызывающему кодуB)Возвращает значение по умолчанию для типа результатаC)Завершает весь процесс с кодом возврата ошибкиD)Паникует и валит поток, печатая содержимое ошибки
показать ответ и разбор
+D)Паникует и валит поток, печатая содержимое ошибки// разбор: unwrap разворачивает Ok, а на Err паникует с сообщением called Result::unwrap() on an Err value и Debug-выводом ошибки. Паника валит текущий поток, а не процесс: в main это выглядит как падение программы, в рабочем потоке — как его смерть с ошибкой в join. Понятнее expect с описанием ожидания.
- Что делает оператор ? в конце выражения let s = read(path)?;A)Разворачивает значение, а при ошибке паникует, как unwrapB)Пробует выполнить выражение и подставляет значение по умолчанию при сбоеC)Помечает выражение как способное вернуть ошибку, не меняя потокD)При Ok разворачивает значение, при Err выходит из функции с этой ошибкой
показать ответ и разбор
+D)При Ok разворачивает значение, при Err выходит из функции с этой ошибкой// разбор: Это ранний выход: Ok(v) даёт v, Err(e) немедленно возвращает Err(e) из текущей функции. Работает только там, где тип возврата это позволяет — Result, Option или другой тип с Try. Именно ? делает код с ошибками линейным: счастливый путь читается сверху вниз без лестницы match.
- Чем Result<T, E> отличается от Option<T> по смыслу?A)Result используют в библиотеках, Option — в приложенияхB)Result проверяется компилятором, Option можно игнорировать молчаC)Result несёт причину неудачи, Option — факт отсутствияD)Result работает с типами из std, Option — с пользовательскими
показать ответ и разбор
+C)Result несёт причину неудачи, Option — факт отсутствия// разбор: Option отвечает на вопрос «есть значение или нет» — пустой список, ключ не найден, поле не заполнено. Result отвечает «получилось или нет и почему» — диск не читается, строка не разбирается. Между ними ходят через ok_or (Option в Result) и ok (Result в Option).
- Зачем ошибки приложения собирают в один enum с вариантами вместо строк?A)Enum занимает меньше памяти, чем String с текстом ошибкиB)Строку компилятор не примет как тип ошибки в ResultC)Вызывающий разбирает вариант через match и реагирует по-разномуD)Только enum можно преобразовать через оператор ?
показать ответ и разбор
+C)Вызывающий разбирает вариант через match и реагирует по-разному// разбор: Строка годится только для лога: по ней не решить, ретраить запрос или показать пользователю форму. Enum делает исходы частью типа — NotFound, Timeout, Invalid(field) разбираются match, а компилятор напоминает про новые варианты. Текст при этом никуда не девается, он живёт в Display.
- Что даёт .context("чтение конфига") у anyhow?A)Помечает ошибку как некритичную и продолжает выполнениеB)Заменяет текст ошибки на переданныйC)Добавляет к ошибке слой описания, сохраняя исходную причинуD)Отправляет ошибку в систему трассировки вместе со стеком
показать ответ и разбор
+C)Добавляет к ошибке слой описания, сохраняя исходную причину// разбор: context надстраивает цепочку: наверху человеческое «чтение конфига», ниже — исходная io-ошибка, ещё ниже её причина. В логе это печатается лесенкой Caused by, и по ней видно и что делали, и на чём споткнулись. Исходная ошибка при этом остаётся доступной для разбора.
- Чем expect("нужен конфиг") отличается от unwrap() при ошибке?A)Возвращает переданную строку как значение вместо паникиB)Печатает переданное сообщение вместе с содержимым ошибкиC)Заменяет ошибку своим текстом и передаёт её наверхD)Паникует только в debug-сборке, в release просто пишет в лог
показать ответ и разбор
+B)Печатает переданное сообщение вместе с содержимым ошибки// разбор: Поведение то же самое — паника, отличается лишь сообщение: expect добавляет строку автора, unwrap оставляет стандартный текст. В сообщение пишут не «что-то пошло не так», а ожидание, которое нарушено: «конфиг должен быть прочитан на старте» — по такому тексту причину видно сразу из лога.
- Функция возвращает Result<i32, ParseIntError>, а внутри стоит ? на чтении файла. Что скажет компилятор?A)Соберёт: ? приводит ошибку к типу возврата функцииB)E0277: ошибку ввода-вывода не преобразовать в ParseIntErrorC)Соберёт с предупреждением о потере исходного типа ошибкиD)E0308: несовпадение типов в возвращаемом значении функции
показать ответ и разбор
+B)E0277: ошибку ввода-вывода не преобразовать в ParseIntError// разбор: Оператор ? делает конверсию через From: чтобы вернуть io::Error из функции с ParseIntError, нужна реализация From<io::Error> for ParseIntError — её нет и быть не может. Выходы: свой enum ошибок с From на оба случая, Box<dyn Error> или anyhow в прикладном коде.
это 9 из 60
Ещё 51 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Что делает оператор ?
Разворачивает Ok или Some, а в случае Err выходит из функции, предварительно преобразовав ошибку через From в тип, объявленный в сигнатуре. Именно это преобразование чаще всего и ломает компиляцию: нужного impl From просто нет.
thiserror или anyhow?
thiserror пишут в библиотеках: он генерирует конкретный тип ошибки, с которым вызывающий код сможет работать через match. anyhow — для приложений, где важнее донести контекст до лога, а разбирать причину по вариантам никто не будет.
Когда допустим unwrap?
Когда None или Err означают сломанный инвариант программы, а не внешний сбой: тесты, прототипы, разбор константы, известной на этапе компиляции. В коде, обрабатывающем ввод, сеть или файлы, unwrap — это отложенная паника в проде.