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