Курс ЛР7 · Місія 7
Академічна↔ Сюжет С.І.Д.СРС до ЛР⌨️ команди 3

Лабораторна робота № 7. Читання файлу з обробкою помилок

Мета

Навчитися реалізовувати читання файлу з повною обробкою помилок через Result, власний тип помилки, оператор поширення помилки ? і, на розширеному рівні, побудувати систему тихого логування збоїв замість аварійного завершення програми.

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

Теоретичні відомості

Result<T, E> — стандартний спосіб Rust позначити операцію, що може завершитись успіхом (Ok(T)) або провалом (Err(E)). На відміну від винятків (exceptions) в інших мовах, можливість провалу видно прямо в сигнатурі функції — виклик функції, що повертає Result, компілятор попереджає (#[must_use]), якщо результат проігноровано.

Власний тип помилки — зазвичай enum, що об'єднує різні категорії збоїв в одному місці:

#[derive(Debug)]
enum ReadLogError {
    NotFound(String),
    PermissionDenied,
    Corrupted { offset: usize },
}

Це дозволяє викликачу функції розрізняти категорії помилок через match, а не парсити текстове повідомлення. Тип помилки може реалізувати трейт std::error::Error (і зазвичай std::fmt:: Display), щоб інтегруватися зі стандартними інструментами обробки помилок і з Box<dyn Error>.

Оператор ? застосований до виразу типу Result<T, E> усередині функції, яка сама повертає Result<_, E2> (де E конвертується в E2 через трейт From), розгортає Ok(v) у v і, у випадку Err(e), негайно виконує return Err(e.into()) з поточної функції. Це прибирає ланцюжки ручних match при послідовних операціях, що кожна може провалитись (наприклад: відкрити файл → прочитати вміст → перевірити кодування). Коли для побудови помилки потрібен додатковий контекст, якого сам вихідний тип помилки не несе (наприклад, шлях до файлу для std::io::Error), природний прийом — .map_err(|e| ...)? замість покладання на автоматичний impl From:

fn read_log(path: &str) -> Result<String, ReadLogError> {
    let bytes = std::fs::read(path).map_err(|e| classify_io_error(path, e))?;
    let text = String::from_utf8(bytes)
        .map_err(|e| ReadLogError::Corrupted { offset: e.utf8_error().valid_up_to() })?;
    Ok(text)
}

panic vs Result: panic! (і його похідні — unwrap(), expect() на Err/None) означає "продовжувати виконання безглуздо або небезпечно" — типово для порушених інваріантів програми, а не для очікуваних збоїв зовнішнього середовища (відсутній файл, побиті дані). Для очікуваних ситуацій, коли виклику варто дати шанс обробити проблему й продовжити роботу — правильний вибір Result, а не panic!.

Тихе логування — замість негайного аварійного завершення чи виведення в stderr, збій записується у фоновий лог-файл (наприклад, дописуванням рядка через std::fs::OpenOptions з прапорцем append(true)), а функція повертає керування викликачу через Result, дозволяючи основному потоку виконання продовжити роботу з іншими файлами/записами.

Виявлення пошкодженого UTF-8: String::from_utf8(bytes: Vec<u8>) -> Result<String, FromUtf8Error> повертає Err, якщо байти не утворюють коректну UTF-8 послідовність. FromUtf8Error::utf8_error().valid_up_to() повертає індекс першого байта, з якого послідовність перестає бути валідною — цю позицію зручно передавати далі як offset у власному типі помилки, щоб діагностика була точною, а не загальною.

Обладнання та програмне забезпечення

Порядок виконання

  1. Відкрити стартовий каркас, ознайомитись зі структурою тестових файлів у starter/README.md.
  2. Визначити власний enum ReadLogError з варіантами під основні категорії збоїв читання файлу (NotFound, PermissionDenied, Corrupted).
  3. Реалізувати fn read_log(path: &str) -> Result<String, ReadLogError>, що відкриває файл і повертає його вміст або відповідний варіант помилки — без жодного panic!/unwrap() на шляху очікуваних збоїв. Зібрати й перевірити базовими тестами на наданих фікстурах.
  4. (Рівень 2) Переписати ланцюжок операцій усередині read_log (і допоміжних функцій) з використанням оператора ? замість ручних match, зберігши однакову поведінку на тих самих трьох фікстурах.
  5. (Рівень 3) Додати функцію тихого логування, яка при кожному Err дописує рядок з описом інциденту у фоновий файл (incidents.log) і повертає Result викликачу, не перериваючи основний потік виконання програми.

Завдання за рівнями

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Рівень 1 (оцінка 3)

Реалізувати читання файлу з власним типом помилки (ReadLogError мінімум із трьома варіантами) та обробкою через Result, без panic!/unwrap() на шляху очікуваних збоїв. Функція повертає Ok для коректного файлу, Err(NotFound(_)) для відсутнього, Err(Corrupted { .. }) для пошкодженого. Три шаблонні юніт-тести на наданих фікстурах мають проходити.

РІВЕНЬ 2 · «4» · МАГІСТР

Рівень 2 (оцінка 4, захист)

Використати оператор поширення помилки (?) для елегантної маршрутизації збоїв у реалізації read_log і допоміжних функцій, зберігши ідентичну поведінку на тих самих фікстурах. На захисті — усно пояснити, як ? перетворює тип помилки при потребі (через From або .map_err(...)?) і чому обраний саме такий підхід у цьому конкретному випадку.

РІВЕНЬ 3 · «5» · ЛЕГЕНДА

Рівень 3 (оцінка 5, розширення)

Створити кастомну систему тихого логування інцидентів читання у фоновий файл: функція на кшталт read_log_logged(path, log_path) при Err дописує рядок-опис інциденту у log_path і повертає той самий Err далі, а виклик, що обробляє список файлів, не переривається аварійно через жоден з них.

🧪 РОЗІБРАНИЙ ПРИКЛАД

Розібраний приклад

Виявлення позиції пошкодження в UTF-8:

fn main() {
    let broken = vec![0x48, 0x69, 0xFF, 0x21]; // "Hi", потім невалідний байт, потім "!"
    match String::from_utf8(broken) {
        Ok(s) => println!("OK: {s}"),
        Err(e) => {
            let offset = e.utf8_error().valid_up_to();
            println!("Пошкоджено з позиції {offset}"); // 2
        }
    }
}

Повна реалізація рівня 2 з ? і збереженим контекстом шляху:

fn classify_io_error(path: &str, e: std::io::Error) -> ReadLogError {
    match e.kind() {
        std::io::ErrorKind::NotFound => ReadLogError::NotFound(path.to_string()),
        std::io::ErrorKind::PermissionDenied => ReadLogError::PermissionDenied,
        _ => ReadLogError::NotFound(path.to_string()),
    }
}

fn read_log(path: &str) -> Result<String, ReadLogError> {
    let bytes = std::fs::read(path).map_err(|e| classify_io_error(path, e))?;
    let text = String::from_utf8(bytes)
        .map_err(|e| ReadLogError::Corrupted { offset: e.utf8_error().valid_up_to() })?;
    Ok(text)
}

Команди перевірки:

cargo build --manifest-path minefield/Cargo.toml
cargo test --manifest-path minefield/Cargo.toml
❓ КОНТРОЛЬНІ ПИТАННЯ

Контрольні питання

  1. Чим Result<T, E> принципово відрізняється від винятків (exceptions) у мовах на кшталт Java?
  2. Коли доречний panic!, а коли — повернення Err через Result?
  3. Що конкретно робить оператор ?, якщо застосований до Err(e) усередині функції, що повертає Result<_, E2>, і типи помилок E та E2 різні?
  4. Навіщо реалізовувати трейт std::error::Error для власного типу помилки?
  5. Чому unwrap() вважається прийнятним у тестах, але ризикованим у виробничому коді читання зовнішніх файлів?
  6. Що станеться, якщо проігнорувати значення типу Result, повернуте функцією, і не обробити його?
  7. Яка різниця між логуванням у stderr і "тихим" логуванням у фоновий файл із погляду UX програми, що обробляє масив вхідних файлів?
  8. Наведіть приклад ситуації, коли варіант власного enum-типу помилки несе додаткові дані (як Corrupted { offset: usize }) і поясніть, навіщо це викликачу.

Парні теми СРС

Критерії оцінювання та форма звіту

Студент здає лабораторну тегом submit/lab07. Автотести рівня 1 (autograder/) перевіряють: проєкт збирається; read_log повертає Ok для коректного тестового файлу і Err відповідного варіанта для навмисно відсутнього/пошкодженого файлу (перевіряється кількома тестовими фікстурами); у коді відсутні unwrap()/panic!() на шляху обробки очікуваних збоїв (текстовий аналіз тіла read_log).

Для рівня 2 автотести перевіряють наявність оператора ? у реалізації (а не суто ручних match) і те, що поведінка (набір Ok/Err на тому самому наборі тестових файлів) не змінилась порівняно з рівнем 1. Захист рівня 2 — усне пояснення, як ? перетворює тип помилки при потребі (через From).

Рівень 3 оцінюється вручну: перевіряється, що після серії викликів на пошкоджених файлах основна програма не завершується аварійно, у incidents.log з'являється по одному рядку на кожен збій із достатньою діагностичною інформацією (шлях до файлу, категорія помилки), і що логування не блокує/не уповільнює обробку решти файлів помітно.

Форма звіту: короткий опис реалізації (3–5 речень) з поясненням трьох категорій ReadLogError, скріншот терміналу із зеленим cargo test, для рівня 2 — фрагмент коду з ?, для рівня 3 — приклад вмісту incidents.log після прогону на кількох пошкоджених файлах.

⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 3

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

cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo test
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.

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