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

Лабораторна робота № 5. Набір програм, що не компілюються (borrow checker)

Мета

Навчитися читати повідомлення компілятора Rust про порушення правил володіння й запозичень, діагностувати причину помилки компіляції та виправляти її мінімальними, обґрунтованими змінами, включно з випадками конфлікту часів життя (lifetimes) і надлишкового копіювання даних.

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

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

Модель володіння (ownership) — набір статичних правил Rust, які компілятор перевіряє під час компіляції, без затримки виконання й без збирача сміття: кожне значення має рівно одного власника; коли значення передається у функцію за значенням (не через посилання), відбувається переміщення (move) — попередня змінна втрачає право доступу до значення. Спроба використати змінну після переміщення дає помилку компіляції E0382 ("use of moved value") — не попередження, а жорстку відмову збирати програму.

Borrow checker — та частина rustc, що перевіряє правила запозичень: у будь-який момент часу для одного значення може існувати або довільна кількість незмінних посилань (&T), або рівно одне змінне (&mut T), і жодне з них не може пережити дані, на які вказує. Порушення дає помилки з кодами на кшталт E0502 (конфлікт незмінного й змінного запозичення в одній області видимості) чи E0499 (кілька змінних запозичень одночасно).

Мінімальне виправлення E0382 зазвичай не потребує .clone(). Найчастіша причина хибного переміщення — допоміжна функція приймає значення за володінням (String), хоча насправді лише читає його вміст і могла б приймати запозичення (&str):

// Проблема: process(log) переміщує log, друга спроба використання — помилка
fn process(s: String) -> String { s.to_uppercase() }

// Виправлення: process лише позичає — log лишається дійсним після виклику
fn process(s: &str) -> String { s.to_uppercase() }

Це змінює контракт лише внутрішньої допоміжної функції, не чіпаючи публічну сигнатуру й логіку алгоритму — саме такого мінімалізму вимагає діагностична робота з чужим (чи "мертвим") кодом.

Час життя (lifetime) — параметр типу посилання, що описує, наскільки довго воно лишається дійсним. У переважній більшості випадків компілятор виводить часи життя автоматично за правилами lifetime elision. Але коли функція повертає посилання, походження якого залежить від того, яке саме з кількох вхідних посилань проживе довше — компілятор вимагає явної анотації в сигнатурі:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}

Явний час життя не змінює фактичний час життя значення в пам'яті й не подовжує його — він лише повідомляє компілятору зв'язок між часами життя різних посилань (тут: результат живе не довше за менше з двох вхідних 'a), щоб той міг статично довести безпеку коду.

Типова стратегія діагностики:

  1. Прочитати повідомлення компілятора повністю, звернувши увагу на рядок і стовпець, куди вказує ^^^, і на службові нотатки (note:), які сучасний rustc часто додає з підказкою напряму виправлення.
  2. Визначити, яке саме значення "втрачено" (moved) чи яке запозичення конфліктує.
  3. Отримати розгорнуте пояснення командою rustc --explain <код> (наприклад, rustc --explain E0382).
  4. Знайти мінімальну зміну: запозичення замість переміщення, звуження області видимості блоком { }, зміна порядку операцій або явна анотація часу життя — не переписування логіки з нуля.

Надлишкове копіювання (.clone() "про всяк випадок") часто працює, компілюється й навіть проходить тести — але ціною зайвих виділень пам'яті й копіювань даних на кожному кроці обробки. Рефакторинг після відновлення роботоздатності коду замінює такі місця на роботу через посилання (&T, ітератори .iter()), коли повне володіння копією насправді не потрібне.

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

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

  1. Відкрити наданий стартовий проєкт (starter/), що містить кілька модулів з навмисними помилками компіляції.
  2. Запустити cargo build --manifest-path dead_log/Cargo.toml і зберегти повний текст помилки для level1_ownership.rs.
  3. Використати rustc --explain E0382 для отримання розширеного офіційного пояснення.
  4. Виправити помилку рівня 1 мінімальною зміною (запозичення замість переміщення в допоміжній функції), повторно зібрати проєкт — очікуваний результат: cargo build без помилок, cargo test decode_fragment_smoke — зелений.
  5. (Рівень 2) Перейти до level2_lifetimes.rs, проаналізувати, чому компілятор не може вивести час життя самостійно, і додати коректну анотацію 'a в сигнатуру pick_longer_coordinate.
  6. (Рівень 3) Після відновлення роботоздатності фрагмента — провести рефакторинг level3_refactor.rs для усунення зайвих .clone(), виміряти різницю в часі виконання наданим мікробенчмарком, задокументувати результат.

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

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

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

Діагностувати причину помилки компіляції E0382 у level1_ownership.rs та виправити її мінімальною зміною (без переписування логіки алгоритму, без видалення другого використання log), зберігши сигнатуру fn decode_fragment(log: String) -> String і поведінку супровідного тесту decode_fragment_smoke.

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

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

Розібрати конфлікт часів життя в level2_lifetimes.rs: додати явну анотацію 'a в сигнатуру pick_longer_coordinate, щоб функція компілювалась і коректно відображала залежність вихідного посилання від обох вхідних. На захисті — усно пояснити, чому компілятор не міг вивести час життя автоматично й що саме означає додана анотація.

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

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

Провести повний рефакторинг відновленого фрагмента level3_refactor.rs, усунувши зайві .clone()/копіювання, замінивши їх роботою через позики та ітератори там, де повне володіння не потрібне. Виміряти й задокументувати вимірне пришвидшення наданим мікробенчмарком, зберігши попередню поведінку (ті самі тести проходять).

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

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

Повне повідомлення компілятора для level1_ownership.rs (скорочено):

error[E0382]: borrow of moved value: `log`
 --> src/level1_ownership.rs:3:41
  |
1 | pub fn decode_fragment(log: String) -> String {
  |                         --- move occurs because `log` has type `String`
2 |     let decoded = process(log);
  |                           --- value moved here
3 |     format!("{} / оригінал: {}", decoded, log)
  |                                            ^^^ value borrowed here after move
  |
  = note: consider changing this parameter type in `process` to borrow instead

Структура повідомлення: код помилки й короткий опис (перший рядок), точна адреса файл:рядок: стовпець, місце переміщення (value moved here) і місце повторного використання (value borrowed here after move), а часто й службова підказка (note:), що вказує напрям виправлення. Мінімальне виправлення — змінити тип параметра допоміжної функції process з String на &str:

fn process(s: &str) -> String { s.to_uppercase() }
// виклик: process(&log) замість process(log)

Отримати розгорнуте офіційне пояснення коду помилки:

rustc --explain E0382
❓ КОНТРОЛЬНІ ПИТАННЯ

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

  1. Що означає код помилки E0382 і за яких умов компілятор його видає?
  2. Чим E0502 відрізняється від E0499?
  3. Що таке lifetime elision і коли компілятор НЕ може застосувати його автоматично?
  4. Чи змінює анотація 'a реальний час життя значення? Що вона насправді повідомляє компілятору?
  5. Наведіть приклад мінімальної зміни (без переписування логіки), яка усуває помилку E0382.
  6. Чому .clone() — це часто "швидке", але не завжди найкраще виправлення конфлікту запозичень?
  7. Що виводить команда rustc --explain <код> і чим вона корисна під час діагностики?
  8. Як звуження області видимості запозичення блоком { } може усунути конфлікт E0502?

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

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

Студент здає лабораторну тегом submit/lab05. Автотести рівня 1 (autograder/) перевіряють, що всі модулі стартового каркаса, позначені як "рівень 1", після правок компілюються без помилок (cargo build з кодом виходу 0), а супровідні юніт-тести до цих модулів проходять — це гарантує, що виправлення не просто "обійшло" помилку, а зберегло коректну поведінку.

Для рівня 2 автотести перевіряють компільованість модуля з lifetime-конфліктом і наявність явної анотації часу життя в сигнатурі (а не, наприклад, підміну логіки на 'static без потреби). Захист рівня 2 — усне пояснення, чому компілятор не міг вивести час життя сам і що саме означає додана анотація.

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

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

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

rustc
Компілятор Rust; напряму викликається рідко (rustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.
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.