Свои типы ошибок в Rust
Дальше базового уровня спрашивают, как устроен собственный тип ошибки: какие трейты нужны, зачем source, почему enum лучше строки. Здесь видно, проектировал ли человек библиотеку или только вызывал чужие.
Стержень: ошибка это тип, который вызывающий может разобрать, а не текст, который можно только напечатать.
// Формулировки: «что нужно реализовать своему типу ошибки?», «зачем source?», «строка или enum?»
Что требует трейт Error
std::error::Error требует Debug и Display как супертрейты: первый нужен для {:?} и вывода из main, второй - для человекочитаемого сообщения. Сам трейт добавляет метод source() - ссылку на нижележащую причину.
Из source складывается цепочка: ConfigError указывает на io::Error, тот на свою причину. Обходя её, логгер печатает всю историю сбоя, а не только верхнее сообщение. Важное правило: Display описывает только свой уровень, иначе текст задублируется на каждом шаге цепочки.
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.inner)
}
}- source
- нижележащая причина ошибки
- цепочка причин
- последовательность вложенных ошибок от верхней к корневой
Enum вместо строки
Строковая ошибка годится только для лога: по тексту нельзя решить, ретраить запрос или показать пользователю форму. Enum делает исходы частью типа - NotFound, Timeout, Invalid(field) разбираются через match, а компилятор напоминает про новые варианты.
Реализация From<io::Error> для своего типа включает автоматическую конверсию в операторе ?, и код перестаёт обрастать map_err. Полезно сохранять внутри варианта саму исходную ошибку, а не только её текст: тогда цепочка причин остаётся полной.
// Публичный enum ошибок помечают #[non_exhaustive]: набор причин сбоя растёт вместе с библиотекой, и это не должно быть ломающим изменением.
#[derive(Debug)]
enum AppError {
NotFound,
Io(std::io::Error),
}- enum ошибок
- перечисление исходов, разбираемое вызывающим
- From для источника
- реализация, позволяющая ? конвертировать ошибку
Как отвечать: «Как ты проектируешь тип ошибки для библиотеки?»
Делаю enum с вариантами по исходам, которые вызывающему полезно различать - NotFound, Timeout, Invalid с полем. Реализую Debug и Display, потому что они супертрейты Error, и сам Error с методом source, чтобы сохранялась цепочка причин: в вариант кладу исходную ошибку целиком, а не её текст. На каждый внешний источник пишу From - тогда в коде работает просто ? без map_err. Публичный enum помечаю non_exhaustive, чтобы добавление новой причины не ломало пользователей. В Display описываю только свой уровень, иначе сообщение задублируется вместе с вложенным.
Это ответ человека, который выпускал библиотеку: есть и структура типа, и совместимость по семверу, и аккуратность с текстами в цепочке.
На чём валятся
- −Возвращают ошибку строкой, и вызывающему остаётся сравнивать текст.
- −Забывают про source, и в логе видно только верхнее сообщение.
- −Дублируют в Display текст вложенной ошибки - цепочка печатает одно и то же дважды.
- −Не помечают публичный enum non_exhaustive и ломают пользователей минорным релизом.
- −Кладут в вариант только текст исходной ошибки, теряя возможность разобрать её тип.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Зачем для своего типа ошибки реализуют From<io::Error>?A)Чтобы своя ошибка наследовала поведение ошибки ввода-выводаB)Чтобы io::Error можно было сравнить со своей ошибкойC)Чтобы компилятор разрешил вернуть io::Error напрямуюD)Чтобы ? сам заворачивал ошибку ввода-вывода в свой тип
показать ответ и разбор
+D)Чтобы ? сам заворачивал ошибку ввода-вывода в свой тип// разбор: Оператор ? вызывает From для приведения к типу возврата: с реализацией From<io::Error> строка let f = File::open(p)? работает без map_err. Одна реализация на каждый внешний источник ошибок — и функции остаются линейными, а обёртывание живёт в одном месте.
- Что даёт метод source() в реализации Error?A)Место в коде, где ошибка была созданаB)Ссылку на нижележащую ошибку — это цепочка причинC)Строку с полным текстом ошибки и всех её причинD)Копию исходной ошибки для повторного возврата
показать ответ и разбор
+B)Ссылку на нижележащую ошибку — это цепочка причин// разбор: source отвечает на вопрос «а из-за чего это случилось»: обёртка ConfigError указывает на io::Error, тот — на причину ниже. Обходя цепочку, логгер печатает полную историю сбоя вместо одного верхнего сообщения. Display при этом должен описывать только свой уровень, иначе текст задублируется.
- Библиотека возвращает enum ошибок. Почему его помечают #[non_exhaustive]?A)Чтобы новый вариант ошибки не ломал сборку пользователейB)Чтобы enum занимал столько же места после расширенияC)Чтобы пользователи не создавали значения этих ошибок самиD)Чтобы варианты можно было добавлять без реализации Display
показать ответ и разбор
+A)Чтобы новый вариант ошибки не ломал сборку пользователей// разбор: Набор причин сбоя растёт вместе с библиотекой: появился новый бэкенд — появился новый вариант. Без атрибута любой исчерпывающий match у пользователя перестанет собираться, то есть минорный релиз станет ломающим. С ним внешний код обязан держать ветку _, и расширение проходит бесшовно.
- Где проходит граница между Result и panic по смыслу?A)Result — в библиотеках, panic — в приложениях и утилитахB)Result — для ошибок ввода-вывода, panic — для всего остальногоC)Result — ожидаемый сбой, panic — нарушенный инвариант программыD)Result — когда ошибку обрабатывают, panic — когда её логируют
показать ответ и разбор
+C)Result — ожидаемый сбой, panic — нарушенный инвариант программы// разбор: Файла нет, сеть отвалилась, пользователь прислал мусор — это ожидаемые исходы, их место в Result. Индекс за границей массива или невозможное состояние — признак бага, и продолжать работу опаснее, чем упасть. Поэтому библиотеки почти всегда возвращают Result: решать, падать ли, должен вызывающий.
- Зачем типу ошибки реализация Display, если уже есть Debug?A)Display генерируется автоматически, если у типа есть реализация DebugB)Display обязателен для оператора ?, а Debug используется только в тестахC)Display включает цепочку причин целиком, а Debug ограничен только верхним уровнемD)Display показывают пользователю и пишут в лог, Debug нужен для разработчика
показать ответ и разбор
+D)Display показывают пользователю и пишут в лог, Debug нужен для разработчика// разбор: Debug выводит структуру значения и удобен при отладке, но пользователю показывать его нечего: Os { code: 2, kind: NotFound } мало что объясняет. Display даёт человеческую формулировку — «не найден файл конфигурации». Трейт Error требует обоих, потому что ошибка живёт в двух мирах: в логах разработчика и в сообщениях наружу. Автоматически Display не выводится, его пишут руками или генерируют макросом.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.