Программирование RustОбработка ошибокРазработчик библиотек на Rust

Представьте публичную библиотеку Rust: как объявить перечисление ошибок, чтобы добавление нового варианта н...

Представьте публичную библиотеку Rust: как объявить перечисление ошибок, чтобы добавление нового варианта не ломало сопоставление у клиентов?

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

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

Для публичного перечисления ошибок следует использовать атрибут #[non_exhaustive]. Внешний код при сопоставлении обязан предусмотреть запасную ветку, поэтому библиотека сможет добавлять новые варианты без нарушения совместимости исходного кода клиентов.

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

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

#[non_exhaustive] предназначен для API, которое может расширяться. Он переносит часть контроля с компилятора на разработчика клиента: тот должен явно определить поведение для неизвестных в будущем вариантов.

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

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

Если же клиент использует запасную ветку, он компилируется после добавления варианта, но должен корректно обработать неизвестную ошибку. Неверно выбранное поведение может скрыть новую причину, привести к неправильному сообщению пользователю или нарушить повторные попытки.

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

Атрибут #[non_exhaustive] на перечислении запрещает внешним по отношению к определяющему ящику клиентам считать сопоставление исчерпывающим. Клиент может обрабатывать известные варианты, но обязан добавить запасную ветку.

// библиотека #[non_exhaustive] pub enum StorageError { NotFound, PermissionDenied, } // клиент fn describe(error: StorageError) -> &'static str { match error { StorageError::NotFound => "объект не найден", StorageError::PermissionDenied => "нет доступа", _ => "неизвестная ошибка хранения", } }

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

Главный компромисс — между расширяемостью и строгой проверкой. Исчерпывающее сопоставление сразу показывает, что обработаны все известные случаи, но делает добавление вариантов потенциально ломающим изменением. #[non_exhaustive] сохраняет совместимость компиляции, однако неизвестные варианты могут попасть в менее точную обработку.

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

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

Библиотека доступа к хранилищу возвращает публичный тип ошибки. Команда ожидает, что со временем появятся варианты вроде ошибки тайм-аута, лимита запросов или временной недоступности сервера.

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

Второй вариант — публичный enum с #[non_exhaustive]. Клиенты обязаны иметь запасную ветку, поэтому новые варианты не ломают их компиляцию. Недостаток — клиент может временно обрабатывать новую ошибку слишком обобщённо и должен явно выбрать безопасную политику для неизвестных случаев.

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

Для расширяемой библиотеки обычно выбирают #[non_exhaustive] вместе с документированной политикой обработки неизвестных вариантов. В результате обновление библиотеки сохраняет сборку клиентов, а новые причины можно постепенно обрабатывать специализированно.

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

  1. Распространяется ли #[non_exhaustive] на код внутри того же ящика?

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

  1. Достаточно ли добавить запасную ветку, если неизвестная ошибка должна повторяться автоматически?

Нет. Запасная ветка только обеспечивает компиляцию и задаёт реакцию на неизвестный вариант. Для повторных попыток, возврата HTTP-статуса или показа сообщения нужно определить семантику: неизвестная ошибка может быть временной, постоянной или требующей немедленного прекращения операции. Нельзя безопасно выводить эту политику только из факта наличия #[non_exhaustive].

  1. Зачем использовать #[non_exhaustive], если библиотека уже возвращает Box<dyn std::error::Error>?

У динамической ошибки клиент обычно и так не сопоставляет конкретное перечисление без дополнительных механизмов. #[non_exhaustive] важен именно для публичных перечислений, которые клиенты должны различать по вариантам. Если библиотека предоставляет конкретный тип ошибки в публичном API, атрибут делает намерение о возможности будущего расширения явным и защищает клиентов от исчерпывающих сопоставлений.