Лабораторна робота № 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 у власному типі помилки,
щоб діагностика була точною, а не загальною.
Обладнання та програмне забезпечення
- Codespaces / devcontainer курсу з Rust toolchain
- Наданий стартовий каркас
starter/із заготовленими "битими" тестовими файлами
Порядок виконання
- Відкрити стартовий каркас, ознайомитись зі структурою тестових файлів у
starter/README.md. - Визначити власний
enum ReadLogErrorз варіантами під основні категорії збоїв читання файлу (NotFound,PermissionDenied,Corrupted). - Реалізувати
fn read_log(path: &str) -> Result<String, ReadLogError>, що відкриває файл і повертає його вміст або відповідний варіант помилки — без жодногоpanic!/unwrap()на шляху очікуваних збоїв. Зібрати й перевірити базовими тестами на наданих фікстурах. - (Рівень 2) Переписати ланцюжок операцій усередині
read_log(і допоміжних функцій) з використанням оператора?замість ручнихmatch, зберігши однакову поведінку на тих самих трьох фікстурах. - (Рівень 3) Додати функцію тихого логування, яка при кожному
Errдописує рядок з описом інциденту у фоновий файл (incidents.log) і повертаєResultвикликачу, не перериваючи основний потік виконання програми.
Завдання за рівнями
Рівень 1 (оцінка 3)
Реалізувати читання файлу з власним типом помилки (ReadLogError мінімум із трьома варіантами)
та обробкою через Result, без panic!/unwrap() на шляху очікуваних збоїв. Функція
повертає Ok для коректного файлу, Err(NotFound(_)) для відсутнього, Err(Corrupted { .. })
для пошкодженого. Три шаблонні юніт-тести на наданих фікстурах мають проходити.
Рівень 2 (оцінка 4, захист)
Використати оператор поширення помилки (?) для елегантної маршрутизації збоїв у реалізації
read_log і допоміжних функцій, зберігши ідентичну поведінку на тих самих фікстурах. На
захисті — усно пояснити, як ? перетворює тип помилки при потребі (через From або
.map_err(...)?) і чому обраний саме такий підхід у цьому конкретному випадку.
Рівень 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
Контрольні питання
- Чим
Result<T, E>принципово відрізняється від винятків (exceptions) у мовах на кшталт Java? - Коли доречний
panic!, а коли — поверненняErrчерезResult? - Що конкретно робить оператор
?, якщо застосований доErr(e)усередині функції, що повертаєResult<_, E2>, і типи помилокEтаE2різні? - Навіщо реалізовувати трейт
std::error::Errorдля власного типу помилки? - Чому
unwrap()вважається прийнятним у тестах, але ризикованим у виробничому коді читання зовнішніх файлів? - Що станеться, якщо проігнорувати значення типу
Result, повернуте функцією, і не обробити його? - Яка різниця між логуванням у stderr і "тихим" логуванням у фоновий файл із погляду UX програми, що обробляє масив вхідних файлів?
- Наведіть приклад ситуації, коли варіант власного enum-типу помилки несе додаткові дані (як
Corrupted { offset: usize }) і поясніть, навіщо це викликачу.
Парні теми СРС
- Рівень 2: srs13 — Result проти panic: коли доречна відмова, а коли зупинка
- Рівень 3: srs14 — Власні типи помилок і trait std::error::Error
Критерії оцінювання та форма звіту
Студент здає лабораторну тегом 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 після прогону на кількох пошкоджених файлах.