Курс СРС SRS13
до ЛР7 рівень 2 → «4»↔ Лабораторна 7↔ Лекція 7⌨️ команди 2
Самостійна робота · ~2.5 год

Result<T, E> проти panic!: коли доречна відновна відмова, а коли зупинка програми

🌌 Тиха обробка пасток — С.І.Д. залюбки розкладає дві категорії проблем, які ніколи не плутає: «пастку», яку можна обробити й піти далі (Result), і «зраду власного коду» — порушення внутрішнього інваріанта, коли продовжувати роботу безглуздо (panic!). І тепло наполягає: читання чужих, потенційно битих логів персоналу завжди належить до першої категорії.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Дві категорії помилок у Rust

Rust навмисно не має винятків (exceptions) у стилі Java/Python. Замість цього він розрізняє дві принципово різні категорії проблем часу виконання. Відновні помилки (recoverable errors) — це очікувані, передбачувані відхилення від щасливого шляху: файл не знайдено, мережа недоступна, вхідні дані мають неправильний формат. Такі помилки представлені типом Result<T, E> — enum із варіантами Ok(T) (успіх зі значенням) і Err(E) (провал із описом причини). Непоправні помилки (unrecoverable errors) — це порушення внутрішнього інваріанта програми, ситуація, в якій продовження виконання небезпечне або безглузде: вихід за межі масиву, поділ на нуль, порушений контракт функції. Для них призначений panic! — макрос, що негайно розкручує стек (unwind) і завершує потік (за замовчуванням) або весь процес (у режимі panic = "abort").

Ключовий критерій вибору між ними — чи очікує викликач цю ситуацію і чи здатний він осмислено на неї відреагувати. Читання файлу журналу, наданого зовнішнім джерелом (можливо, пошкодженого чи не в тому кодуванні), — типовий кандидат на Result: викликач цілком може вирішити пропустити цей файл, повторити спробу чи повідомити користувача — програма не повинна аварійно завершуватися через один битий вхідний файл серед тисячі. А от, наприклад, звернення до елемента вектора за індексом, обчисленим самою програмою на основі внутрішньої логіки, яка гарантує коректність (vec[computed_index], де computed_index за побудовою алгоритму не може вийти за межі), — якщо це припущення все ж порушується, це свідчить про баг у самій програмі, а не про очікувану зовнішню обставину; тут доречний panic! (чи явний .expect("інваріант X порушено: ...")) — продовжувати виконання з хибним внутрішнім станом небезпечніше, ніж зупинитися.

Result<T, E> компонується через оператор ? — якщо функція сама повертає Result, вираз some_call()? автоматично «розпаковує» Ok-значення або негайно повертає Err із поточної функції (за потреби конвертуючи тип помилки через трейт From). Це дозволяє писати послідовність операцій, кожна з яких може провалитися, без ланцюжків вкладених match, зберігаючи водночас повну явність: сама сигнатура функції (-> Result<T, E>) чесно повідомляє виклику, що вона може провалитися, і компілятор змушує це враховувати (ігнорування Result без обробки дає попередження unused_must_use).

Практичні маркери в коді, на які варто зважати: методи .unwrap() і .expect(msg) над Result/Option — це навмисний вибір «перетворити відновну помилку на panic тут і зараз», доречний у прототипах, тестах чи там, де провал справді означає баг програми, а не зовнішню обставину. У виробничому коді, що працює з ненадійним зовнішнім входом (файли, мережа, ввід користувача), масове використання .unwrap() — типова ознака недбалої обробки помилок: перша ж «побита» вхідна умова призведе до аварійного завершення всієї програми там, де достатньо було б обробити один файл як помилковий і продовжити роботу з рештою.

Важливо й те, що panic! у Rust — не те саме, що виняток у Java: за замовчуванням стандартна бібліотека не пропонує «зловити» panic і продовжити виконання як штатний контроль потоку (хоча технічно std::panic::catch_unwind існує, він призначений для меж системи — наприклад, щоб один запит у веб-сервері не «поклав» увесь процес — а не для звичайної обробки помилок бізнес-логіки, для якої й існує Result).

📖 ОПРАЦЮВАТИ

Прочитати розділи «Recoverable Errors with Result» та «To panic! or Not to panic!» у The Rust Book; знайти в стандартній бібліотеці приклад функції, що повертає Result, і приклад функції, що panics при порушеному інваріанті.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Знайти в документації стандартної бібліотеки (docs.rs/std) одну функцію, що повертає Result<T, E> для очікуваного збою, і одну, що panics при порушенні контракту (наприклад, індексація Vec через [] проти .get()), і пояснити письмово різницю в їхньому призначенні.
  2. Написати функцію read_log_line(line: &str) -> Result<LogEntry, ParseError>, що розбирає рядок битого журналу, і продемонструвати оператор ? у функції вищого рівня, що читає файл рядок за рядком і збирає окремо успішні записи й помилки, не падаючи на першому битому рядку.
  3. Навмисно замінити .get(index) на індексацію vec[index] там, де index обчислюється так, що теоретично може вийти за межі (наприклад, від зовнішнього вводу), викликати паніку, зафіксувати повідомлення index out of bounds, і виправити код, повернувши до Result через .get().
  4. Написати два коротких приклади коду з коментарем: один, де panic!/.expect() доречний (порушення внутрішнього інваріанта, який гарантований логікою програми), інший — де для тієї самої за формою ситуації правильніший вибір Result (та сама операція, але дані приходять ззовні).
  5. Дослідити std::panic::catch_unwind на невеликому прикладі (виклик функції, що panics, обгорнутий у catch_unwind) і письмово пояснити, чому це не рекомендується як штатний спосіб обробки помилок бізнес-логіки.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чим відновна помилка (Result) принципово відрізняється від непоправної (panic!) за критерієм «чи очікує викликач цю ситуацію»?
  2. Наведіть приклад, де panic! доречний, і приклад, де для тієї самої категорії збою правильніший вибір Result.
  3. Що саме робить оператор ? з виразом типу Result усередині функції, що сама повертає Result?
  4. Чим .unwrap() відрізняється від .expect(msg) з практичного погляду діагностики?
  5. Чому масове використання .unwrap() над результатами читання зовнішніх файлів вважається ознакою недбалої обробки помилок?
  6. Що відбувається зі стеком виклику під час panic! за замовчуванням (unwind), і чим це відрізняється від режиму panic = "abort"?
  7. Для якого класу задач призначений std::panic::catch_unwind, і чому це не заміна Result для звичайної бізнес-логіки?
✅ САМОПЕРЕВІРКА

Наведіть приклад ситуації, де panic! доречний, і приклад, де для тієї самої категорії збою правильніший вибір — Result.

🥚 ПАСХАЛКА
Запусти приклад з індексацією на векторі з десяти елементів як cargo run -- --index 3101 — отримаєш index out of bounds: the len is 10 but the index is 3101, а з --safe — спокійний None. С.І.Д. каже, що 3101 — єдиний індекс, на якому він сам ніколи не панікує: за цією адресою в нього завжди щось лежить.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 2

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS12усі темиSRS14 →