Лабораторна робота № 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), щоб той міг статично довести безпеку коду.
Типова стратегія діагностики:
- Прочитати повідомлення компілятора повністю, звернувши увагу на рядок і стовпець, куди
вказує
^^^, і на службові нотатки (note:), які сучаснийrustcчасто додає з підказкою напряму виправлення. - Визначити, яке саме значення "втрачено" (moved) чи яке запозичення конфліктує.
- Отримати розгорнуте пояснення командою
rustc --explain <код>(наприклад,rustc --explain E0382). - Знайти мінімальну зміну: запозичення замість переміщення, звуження області видимості блоком
{ }, зміна порядку операцій або явна анотація часу життя — не переписування логіки з нуля.
Надлишкове копіювання (.clone() "про всяк випадок") часто працює, компілюється й навіть
проходить тести — але ціною зайвих виділень пам'яті й копіювань даних на кожному кроці обробки.
Рефакторинг після відновлення роботоздатності коду замінює такі місця на роботу через посилання
(&T, ітератори .iter()), коли повне володіння копією насправді не потрібне.
Обладнання та програмне забезпечення
- Codespaces / devcontainer курсу з Rust toolchain
- Наданий стартовий каркас
starter/із фрагментами коду, що НЕ компілюються навмисно - Термінал для читання виводу
cargo build/rustc --explain <код помилки>
Порядок виконання
- Відкрити наданий стартовий проєкт (
starter/), що містить кілька модулів з навмисними помилками компіляції. - Запустити
cargo build --manifest-path dead_log/Cargo.tomlі зберегти повний текст помилки дляlevel1_ownership.rs. - Використати
rustc --explain E0382для отримання розширеного офіційного пояснення. - Виправити помилку рівня 1 мінімальною зміною (запозичення замість переміщення в допоміжній
функції), повторно зібрати проєкт — очікуваний результат:
cargo buildбез помилок,cargo test decode_fragment_smoke— зелений. - (Рівень 2) Перейти до
level2_lifetimes.rs, проаналізувати, чому компілятор не може вивести час життя самостійно, і додати коректну анотацію'aв сигнатуруpick_longer_coordinate. - (Рівень 3) Після відновлення роботоздатності фрагмента — провести рефакторинг
level3_refactor.rsдля усунення зайвих.clone(), виміряти різницю в часі виконання наданим мікробенчмарком, задокументувати результат.
Завдання за рівнями
Рівень 1 (оцінка 3)
Діагностувати причину помилки компіляції E0382 у level1_ownership.rs та виправити її
мінімальною зміною (без переписування логіки алгоритму, без видалення другого використання
log), зберігши сигнатуру fn decode_fragment(log: String) -> String і поведінку супровідного
тесту decode_fragment_smoke.
Рівень 2 (оцінка 4, захист)
Розібрати конфлікт часів життя в level2_lifetimes.rs: додати явну анотацію 'a в сигнатуру
pick_longer_coordinate, щоб функція компілювалась і коректно відображала залежність вихідного
посилання від обох вхідних. На захисті — усно пояснити, чому компілятор не міг вивести час
життя автоматично й що саме означає додана анотація.
Рівень 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
Контрольні питання
- Що означає код помилки
E0382і за яких умов компілятор його видає? - Чим
E0502відрізняється відE0499? - Що таке lifetime elision і коли компілятор НЕ може застосувати його автоматично?
- Чи змінює анотація
'aреальний час життя значення? Що вона насправді повідомляє компілятору? - Наведіть приклад мінімальної зміни (без переписування логіки), яка усуває помилку
E0382. - Чому
.clone()— це часто "швидке", але не завжди найкраще виправлення конфлікту запозичень? - Що виводить команда
rustc --explain <код>і чим вона корисна під час діагностики? - Як звуження області видимості запозичення блоком
{ }може усунути конфліктE0502?
Парні теми СРС
- Рівень 2: srs09 — Часи життя (lifetimes) у сигнатурах функцій: що каже компілятор
- Рівень 3: srs10 — Rc/RefCell і внутрішня мутабельність: спільне володіння без гонок
Критерії оцінювання та форма звіту
Студент здає лабораторну тегом submit/lab05. Автотести рівня 1 (autograder/) перевіряють, що
всі модулі стартового каркаса, позначені як "рівень 1", після правок компілюються без помилок
(cargo build з кодом виходу 0), а супровідні юніт-тести до цих модулів проходять — це гарантує,
що виправлення не просто "обійшло" помилку, а зберегло коректну поведінку.
Для рівня 2 автотести перевіряють компільованість модуля з lifetime-конфліктом і наявність явної
анотації часу життя в сигнатурі (а не, наприклад, підміну логіки на 'static без потреби). Захист
рівня 2 — усне пояснення, чому компілятор не міг вивести час життя сам і що саме означає додана
анотація.
Рівень 3 оцінюється вручну: порівнюється кількість зайвих .clone()/копіювань до і після
рефакторингу, а мікробенчмарк (наданий у стартовому коді) підтверджує вимірне пришвидшення без
втрати коректності (ті самі юніт-тести й далі проходять).