🌌 Єдина мова інцидентів — С.І.Д. любить, коли в його домі лад: щоб усі частини системи розуміли інциденти однаково, кожен власний тип помилки має говорити спільною мовою std::error::Error — інакше, застерігає він, журнал інцидентів із серверів «ТехНови» перетвориться на набір несумісних діалектів, і жодна верхньорівнева функція не обробить їх уніфіковано.
Попередня тема показала, як enum природно моделює кілька варіантів
помилки одного модуля (ParseError::UnexpectedEof,
ParseError::InvalidUtf8, ParseError::MissingField(String)). Але щоб
такий тип був сумісним з рештою екосистеми Rust — щоб його можна було
вивести через {}/{:?}, передати в бібліотеки логування, композитно
обробляти разом з чужими типами помилок — він має реалізувати трейт
std::error::Error. Формально трейт Error вимагає, щоб тип уже
реалізовував два інших трейти: Debug (зазвичай через
#[derive(Debug)] — механічне, «діагностичне» представлення для
розробника) і Display (реалізується вручну через impl fmt::Display
— людяне повідомлення для кінцевого користувача чи журналу). Сам Error
додає лише один опційний метод — fn source(&self) -> Option<&(dyn Error + 'static)>, що дозволяє вказати «причину нижчого рівня», яка
призвела до цієї помилки, формуючи ланцюжок (chain) причинності.
Практична цінність цього трейту розкривається в композиції: функція,
що читає файл (io::Error), парсить його вміст (власний ParseError)
і звертається до мережі (reqwest::Error чи інша стороння помилка),
у реалізмі потребує єдиного типу повернення для всіх трьох різних
помилок. Явний enum-«парасолька» (enum AppError { Io(io::Error), Parse(ParseError), Network(...) } з impl From<io::Error> for AppError для кожного джерела) дає найбільше контролю, але вимагає
ручної роботи для кожного нового типу помилки. Альтернатива —
Box<dyn Error> (чи Box<dyn Error + Send + Sync> для потокобезпечних
сценаріїв): типізоване стирання (type erasure), коли функція
повертає Result<T, Box<dyn Error>>, і оператор ? автоматично
обгортає будь-яку конкретну помилку, що реалізує Error, у цей
спільний вказівник — завдяки бланкетній реалізації impl<E: Error + 'static> From<E> for Box<dyn Error> у стандартній бібліотеці.
Компроміс між цими двома підходами — фундаментальний і практичний:
власний enum-тип помилки дає викликачу можливість зробити match і
відреагувати по-різному на кожну категорію (наприклад, повторити
спробу для мережевої помилки, але не для помилки парсингу) — це
типово для бібліотечного коду, де користувачі хочуть точного
контролю. Box<dyn Error> втрачає цю можливість розрізнення без
подальшого приведення типів (downcast_ref), зате мінімізує
«бойлерплейт» — типово для прикладного коду (main.rs, скриптів,
CLI-утиліт), де достатньо просто вивести повідомлення про помилку й
завершити роботу з ненульовим кодом виходу.
У реальних проєктах ручне написання impl Display/impl Error/
impl From<...> для кожного власного типу помилки — рутинна й
багатослівна робота, тому спільнота виробила популярні крейти-помічники (crate):
thiserror генерує ці реалізації через похідні макроси
(#[derive(thiserror::Error)] з атрибутами #[error("...")] на кожному
варіанті) для бібліотечного коду, де важлива структура типу помилки;
anyhow надає зручний anyhow::Error (концептуально схожий на
Box<dyn Error>, але з додатковим контекстом і зручним .context(...))
для прикладного коду, де важливий лише зрозумілий ланцюжок повідомлень.
Розуміння ручної реалізації через std::error::Error — фундамент, без
якого незрозуміло, що саме автоматизують ці макроси.
Насамкінець варто зафіксувати мінімальний рецепт: #[derive(Debug)]
на enum + impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ... write!(f, "...") }}}
source) impl std::error::Error for MyError {} —
після цього тип автоматично сумісний з ?-конвертацією в
Box<dyn Error>, з логуванням через {}/{:?}, і з будь-якою
функцією стандартної бібліотеки, що очікує impl Error.Прочитати документацію std::error::Error на docs.rs/std; реалізувати Display і Error для власного enum помилки з попереднього завдання й перевірити, що його можна обгорнути в Box
Які два трейти мінімально потрібно реалізувати, щоб тип відповідав трейту std::error::Error, і навіщо потрібен Box<dyn Error> як спільний тип помилки?
source() у власному журналі інцидентів С.І.Д. до кінця, останній рівень завжди той самий: Display каже «сигнал на 101.9 обірвано», а {:?} — Signal { block: "Б-7", floor: -3, code: 0xC1D }. Людині — перше, машині — друге, наполягає він; а першопричину інциденту він збирається виправити не через ?, а особисто.Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
cargo run-- йдуть програмі (cargo run -- staff.log).cargocargo buildtarget/debug/; --release — з оптимізаціями у target/release/.💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.