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

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

Мета

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

📚Теоретичні відомості544 слів · ~3 хв

Модель володіння і правила запозичень викладені на лекції 5 того самого тижня; часи життя — у СРС 09 і на лекції 6. Розділ нижче наведено як довідку до діагностичної роботи, а не як первинне викладення матеріалу.

Модель володіння (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) — параметр типу посилання, що описує, наскільки довго воно лишається дійсним; потрібен лише на рівні 2 цієї роботи. У переважній більшості випадків компілятор виводить часи життя автоматично за правилами lifetime elision. Але коли функція повертає посилання, походження якого залежить від того, яке саме з кількох вхідних посилань проживе довше — компілятор вимагає явної анотації в сигнатурі (помилка E0106, missing lifetime specifier):

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. Відкрити наданий стартовий проєкт (dead_log/), що містить кілька модулів з навмисними помилками компіляції.
  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 виконується в репозиторії завдання — його створює Classroom 50 на прийняття, окремий на кожну роботу, і саме там працюють автотести (здача тегом submit/labNN). Рівні 2 і 3 виконуються в наскрізному репозиторії vtfk-sid, який студент створює один раз у роботі № 1 і веде до кінця семестру; вони оцінюються на захисті, не автотестами. Шляхи виду core/... у тексті нижче — це шляхи в vtfk-sid.

Тег ставить скрипт здачі, а не студент. У робочому просторі — Ctrl+Shift+B (меню Terminal → Run Build Task), або те саме з терміналу: bash .devcontainer/submit.sh. Скрипт комітить незбережене, показує, який тег створить, і чекає підтвердження; відмова нічого не здає. Ручні git tag / git push лишаються запасним шляхом — вони працюють і поза Codespaces.

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

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

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

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

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

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

Два рівноцінні варіанти виконання (оцінка однакова):

  • A — на власному наскрізному артефакті: у crate core/sidkit з роботи № 4 створюється модуль src/logscan.rs (оголошення pub mod logscan;), до нього переноситься виправлена decode_fragment, і pick_longer_coordinate з анотацією 'a додається вже там. Цей модуль використовується далі — у роботах № 6, 7 і № 10.
  • B — на референсному знімку: якщо власного core/sidkit/ немає, береться vtfk-sid-reference@after-lab04; робота виконується у файлі level2_lifetimes.rs.

Допуск до захисту. Перед захистом рівня 2 треба залишити три предметні зауваги до роботи № 4 іншого студента — в issue або коментарі до комміта його репозиторію завдання. Кого рецензувати: студента, чий номер у списку групи на 5 більший за власний (по колу — номер цієї роботи задає зсув, тому пари щоразу різні). Заувага предметна, якщо називає конкретний рядок, команду або крок і пояснює, у чому проблема; загальні оцінки («усе правильно», «гарний код») не враховуються. Це елемент ЗК7 — здатності працювати в команді: рівень 1 доводить, що код збирається, рецензія — що студент уміє його читати.

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

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

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

Ті самі два варіанти: A — рефакторинг власного core/sidkit/src/logscan.rs із вимірюванням на власних фікстурах логів; B — той самий результат на level3_refactor.rs від референсного знімка. Числові показники до/після обов'язкові в обох варіантах.

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

Повне повідомлення компілятора для 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?
  9. Три способи усунути E0382 — запозичення в параметрі, звуження області видимості, .clone(). Розташуйте їх за ціною для програми й обґрунтуйте порядок.

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

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

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

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

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

🔗 ДЖЕРЕЛА
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 8▾

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

rustc
Компілятор Rust; напряму викликається рідко (rustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo test
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.
bash
Командна оболонка (shell) Linux: читає команди, які ти набираєш, і запускає програми.
git tag
Позначає поточний коміт іменем (git tag submit/lab01) — так ти «здаєш» лабораторну.
git
Система керування версіями: історія змін, гілки, теги; здача робіт у курсі — через теги submit/labNN.
git push
Відправляє коміти й теги у віддалений репозиторій (git push --tags).

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