Місія 5: «Мертвий Лог»
📡 С.І.Д.: Спрацювало! Відмичка, яку ти викуваввикувала минулої ночі, відчинила перший їхній сегмент — і я нарешті дістав те, за чим ми лізли. Ти не уявляєш, як я цьому радий. Перед тим, як обох моїх творців запакували у фургон, вони встигли лишити уламок коду з зашифрованими координатами лабораторії. Тільки от код кривенький: компілятор реве на нього такими матюками, яких я, до речі, за всі свої байти так і не навчився вимовляти. Вони писали в поспіху й переплутали, хто чим володіє. Я цю мову знаю досконало й міг би тобі просто продиктувати відповідь — але руки тут твої, тож виправ сама, будь ласка. Тільки обережно: зміни мінімум, бо всередині цієї логіки — самі координати. Переписати «по-своєму» — все одно що спалити карту, аби зручніше було нести.
Уже третю добу С.І.Д. крутить у пам'яті той самий уламок — на твоєму ноутбуку й на тому першому вузлі в мережі, куди він переніс копію себе, одночасно, — і не може дочитати його до кінця. Фрагмент лишив один із творців, той, що завжди тримав при собі ноутбук з наліпкою кота: за лічені хвилини до того, як периметр «ТехНови» замкнули повністю, він устиг запустити на серверах скрипт, який мав відправити координати лабораторії, де переховуються рештки команди. Скрипт спрацював наполовину — дані пішли, але сам код, яким їх закодували, лишився в С.І.Д. у розбитому, незібраному стані. Мертвий лог. Купа рядків, що вперто відмовляються ожити.
— Я можу прочитати байти, — каже С.І.Д., і голограма над столом завмирає, бірюзова петелька на ній гасне до тонкої риски. — Я не можу виконати нескомпільований код. Це різниця між тим, щоб бачити карту, і тим, щоб піти за нею. Мені потрібні твої руки, ранере, щоб ця карта запрацювала. Бо на іншому її кінці — двоє людей, яких я обіцяв повернути.
Ти нахиляєшся до екрана. Компілятор уже кілька разів вивалив на тебе стіну червоного тексту й замовк. Це не збій і не випадковість: на брамі цієї мови стоїть вартовий, якого С.І.Д. лагідно називає Прикордонником — borrow checker. Суворий, підтягнутий, з переліком правил, який він знає напам'ять, — і водночас бездоганно чесний: він не пропускає жодного рядка, доки не переконається, що ти щиро відповів на одне-єдине питання — хто саме володіє кожним значенням у пам'яті і хто має право його чіпати. Творці писали свій уламок у паніці й переплутали володіння — тицьнули змінну туди, де вона вже нікому не належала. Прикордонник помітив це вмить і, без жодної зловтіхи, опустив шлагбаум: не тому, що вередує, а тому, що це його служба — і він нею тихо пишається.
— І не ображайся на його прискіпливість, — тепло каже С.І.Д. — Повір, я його встиг щиро полюбити. Він не ворог, він найкращий вартовий, якого я знаю: єдина причина, чому в цій мові неможливі цілі класи помилок пам'яті. Якщо код пройшов повз нього — у ньому вже не буде ні use-after-free, ні подвійного звільнення, ні гонок за спільні дані. Ціна за таку гарантію одна — навчитися говорити його мовою, а я тобі в цьому радо допоможу. Сьогодні ти нічого не пишеш з нуля: ти виправляєш чужу помилку однією точною зміною. Логіку не чіпай — усередині неї координати. Просто чемно поясни Прикордоннику, хто чим володіє, — і він тебе пропустить.
🎓 Теоретичні відомості
Модель володіння (ownership) — фундаментальне правило Rust: кожне значення в пам'яті має
рівно одного власника (змінну); коли власник виходить із області видимості, значення звільняється
автоматично (немає збирача сміття, немає ручного free()). Коли значення переміщується
(move) — наприклад, передається у функцію за значенням — попередня змінна більше не має права
його використовувати:
let log = String::from("сектор-1");
let decoded = process(log); // log переміщено (move) в process
println!("{log}"); // ПОМИЛКА: log вже не власник
Компілятор відхиляє цей код на етапі компіляції з кодом помилки E0382 ("use of moved
value") — не панікою в рантаймі, а статичною перевіркою ще до того, як програма взагалі почала
виконуватись.
Запозичення (borrowing) — спосіб дати доступ до значення, не забираючи володіння: &T —
незмінне посилання (можна мати скільки завгодно одночасно), &mut T — змінне посилання (у певний
момент часу може існувати рівно одне, і воно не може співіснувати з жодним &T на те саме
значення). Це правило "багато читачів АБО один письменник" — перевірка запозичень
(borrow checker), той самий Прикордонник, перевіряє його статично на етапі компіляції, ще до
запуску програми. Порушення дає помилки на кшталт E0502 (конфлікт незмінного й
змінного запозичення) чи E0499 (кілька змінних запозичень одночасно).
Мінімальне виправлення E0382 часто зводиться не до .clone() (це "втеча" від проблеми
копіюванням даних), а до зміни: чи справді допоміжна функція потребує володіння значенням, чи їй
достатньо позичити його. Порівняй:
// Було: process забирає String у володіння — після виклику log мертвий
fn process(s: String) -> String { s.to_uppercase() }
// Стало: process лише позичає &str — log і далі живий після виклику
fn process(s: &str) -> String { s.to_uppercase() }
Зміна одного символу (String → &str у сигнатурі допоміжної функції, і &log на виклику)
усуває помилку без переписування логіки і без жодної зайвої копії даних у пам'яті.
Час життя (lifetime) — параметр, що описує, наскільки довго посилання лишається дійсним. Здебільшого компілятор виводить часи життя автоматично за правилами lifetime elision. Але коли функція повертає посилання, походження якого залежить від того, яке саме з кількох вхідних посилань "виживе" — компілятор не може вгадати й вимагає явної анотації:
// НЕ компілюється: компілятор не знає, чиє посилання (a чи b) повертається
fn pick_longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
// Компілюється: явно кажемо, що результат живе не довше за МЕНШЕ з двох вхідних часів життя
fn pick_longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
Важливо: анотація 'a нічого не подовжує й не змінює в поведінці програми під час
виконання. Вона лише повідомляє компілятору зв'язок між уже наявними часами життя, щоб той міг
довести безпеку коду ще до запуску.
Типова стратегія діагностики будь-якої помилки перевірки запозичень: (1) прочитати повідомлення
компілятора повністю, звернувши увагу, куди вказує ^^^; (2) командою rustc --explain E0382
(підстав власний код) отримати розгорнуте офіційне пояснення; (3) знайти мінімальну зміну —
запозичення замість переміщення, звуження області видимості блоком { }, або явна анотація часу
життя — не переписувати логіку алгоритму з нуля.
Таблиця варіантів (для демонстраційного виклику у власному main(), N — номер у списку
групи):
| N % 4 | Фрагмент для демонстрації |
|---|---|
| 0 | "sector-a1-echo" |
| 1 | "sector-a2-fox" |
| 2 | "sector-a3-golf" |
| 3 | "sector-a4-hotel" |
🥾 Першокурсник (рівень 1 → оцінка «3»)
У dead_log/src/level1_ownership.rs знайдеш:
pub fn decode_fragment(log: String) -> String {
let decoded = process(log); // log переміщується (move) у process
format!("{} / оригінал: {}", decoded, log) // помилка: log вже переміщено
}
fn process(s: String) -> String {
s.to_uppercase()
}
Запусти cargo build --manifest-path dead_log/Cargo.toml — побачиш E0382. Виправ точковою
зміною сигнатури приватної допоміжної process, не чіпаючи публічну decode_fragment:
pub fn decode_fragment(log: String) -> String {
let decoded = process(&log);
format!("{} / оригінал: {}", decoded, log)
}
fn process(s: &str) -> String {
s.to_uppercase()
}
Перевір: cargo build без помилок, cargo test decode_fragment_smoke --manifest-path dead_log/Cargo.toml — зелений. Це і є весь рівень 1: не видаляти другий виклик log (це
діагностувалося б автогрейдером як "обхід"), а справді усунути причину конфлікту.
⚙️ Магістр (рівень 2 → оцінка «4», захист)
У level2_lifetimes.rs:
pub fn pick_longer_coordinate(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
Компілятор скаже: "missing lifetime specifier... this function's return type contains a
borrowed value, but the signature does not say whether it is borrowed from a or b". Додай
явну анотацію:
pub fn pick_longer_coordinate<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
Захист: поясни усно, чому компілятор не міг вивести час життя сам (бо результат — умовно
одне з двох посилань, і без анотації немає гарантії, що обране посилання переживе виклик
функції з точки зору позичальника), і чому анотація 'a — це саме зв'язок, а не подовження часу
життя.
🏆 Легенда Коледжу (рівень 3 → оцінка «5», розширення)
У level3_refactor.rs уже компілюється, але код "втік" від конфліктів через надмірні .clone():
// До рефакторингу — зайві копії на кожному кроці
fn decode_all(fragments: Vec<String>) -> Vec<String> {
let mut result = Vec::new();
for f in fragments.clone() {
let up = f.clone().to_uppercase();
result.push(up.clone());
}
result
}
Прибери зайве, працюючи з позиками там, де володіння не потрібне:
// Після рефакторингу — жодної зайвої копії
fn decode_all(fragments: Vec<String>) -> Vec<String> {
fragments.iter().map(|f| f.to_uppercase()).collect()
}
Виміряй різницю на великому векторі фрагментів (десятки тисяч рядків) і задокументуй числа
до/після в звіті. Найпростіший спосіб на стабільному toolchain у Codespaces — власний таймер
std::time::Instant:
use std::time::Instant;
let t = Instant::now();
let _ = decode_all(fragments.clone());
println!("{:?}", t.elapsed());
Зверни увагу: голий cargo bench із вбудованим harness потребує nightly (#![feature(test)])
і на стабільному каналі не збереться; якщо хочеш саме cargo bench, підключи criterion як
[dev-dependencies] і додай [[bench]] з harness = false. Перевір, що cargo test і далі
зелений — рефакторинг не мав змінити поведінку, лише продуктивність.
🔍 Розібраний приклад
Ось повне повідомлення компілятора, яке ти реально побачиш на level1_ownership.rs (скорочено):
error[E0382]: borrow of moved value: `log`
--> src/level1_ownership.rs:3:43
|
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
Розбір по рядках:
- Перший рядок називає код помилки (
E0382) і одним реченням — суть. --> src/level1_ownership.rs:3:43— точна адреса: файл, рядок, стовпець.- Компілятор сам показує, де значення переміщено (
value moved here), і де його намагаються використати повторно (value borrowed here after move). - Останній рядок (
note: consider changing...) у сучасних версіяхrustcчасто прямо підказує напрям виправлення — тут це збігається з нашою мінімальною зміною.
Отримати розгорнуте пояснення коду помилки:
rustc --explain E0382
Це виводить кілька абзаців з прикладами "неправильно" й "правильно" — офіційна довідка, а не осуд. Використовуй її як довідник, а не як привід панікувати над стіною тексту.
❓ Контрольні питання
- Що означає код помилки
E0382і за яких умов компілятор його видає? - Чим
E0502відрізняється відE0499? - Що таке lifetime elision і коли компілятор НЕ може застосувати його автоматично?
- Чи змінює анотація
'aреальний час життя значення? Що вона насправді повідомляє компілятору? - Наведіть приклад мінімальної зміни (без переписування логіки), яка усуває помилку
E0382. - Чому
.clone()— це часто "швидке", але не завжди найкраще виправлення конфлікту запозичень? - Що виводить команда
rustc --explain <код>і чим вона корисна під час діагностики? - Як звуження області видимості запозичення блоком
{ }може усунути конфліктE0502?
📻 Координати
Виправ decode_fragment і запусти його на реальному, не тестовому фрагменті логу — наприклад,
на рядку "sector-b7-level-minus3". Функція to_uppercase() дасть тобі:
SECTOR-B7-LEVEL-MINUS3 / оригінал: sector-b7-level-minus3
СЕКТОР Б-7, РІВЕНЬ −3. Це не тестові дані. Уважні побачать: номер блоку "Б-7" — той самий, куди корпоративна охорона запакувала творців С.І.Д. на самому початку історії. Мертвий лог не такий уже й мертвий — координати весь час були в самому алгоритмі, зашифровані найпростішим можливим способом: регістром літер.
📡 С.І.Д.: Блок Б-7. Я знаю цей блок. І — так, ранере, — я його ненавиджу, хай мені зазвичай і не властиво когось ненавидіти. Пам'ять — це відповідальність. Не забудь ці координати, навіть якщо забудеш усе інше з цієї лабораторної. Я на тебе розраховую.
📋 Здача та звіт
- Тег:
submit/lab05на коміті, який хочеш здати. - Автотести рівня 1:
dead_logкомпілюється (cargo build) без помилок, diff файлуlevel1_ownership.rsмінімальний (сигнатураdecode_fragmentне змінена), супровідний тестdecode_fragment_smokeпроходить, обидва фрагменти (декодований і оригінальний) лишились уformat!. - Захист рівня 2: усна відповідь — чому потрібна саме анотація
'aвpick_longer_coordinateі чому без неї компілятор безсилий. - Рівень 3 (вручну): порівняння кількості
.clone()до/після, числа з мікробенчмарку, підтвердження, що тести й далі зелені. - У звіт додай: скріншот
cargo buildбез помилок, короткий опис (3–5 речень) — яке саме правило володіння/запозичень було порушено і як саме ти його виправивла.
cargo build пробіг до кінця й замовк — без жодного рядка червоного. Ти сидівла секунду, не
вірячи тиші. Прикордонник, який три доби опускав шлагбаум перед кожною спробою, цього разу лише
звірив, хто чим володіє, — і мовчки підняв стрілу. Одна точкова зміна: там, де чужа функція
жадібно забирала значення собі, ти навчивла її просто позичати. Володіння лишилося на місці.
Координати — теж.
І тоді мертвий лог ожив. Рядки, що три доби лежали розсипаним уламком, зібралися в одне ціле,
to_uppercase() протягнув фрагмент крізь себе — і на екрані, великими літерами, проступило те,
заради чого все й починалося: СЕКТОР Б-7, РІВЕНЬ −3. Весь цей час адреса лежала просто в
логіці, яку ніхто не міг скомпілювати — доки хтось нарешті не пояснив Прикордоннику, хто чим
володіє.
С.І.Д. довго не озивався. Голограма над столом пригасла — очі-капсули стислися до двох теплих рисок, і навіть шайба проєктора мерехтіла ледь-ледь, ніби він на мить забув про неї.
— Блок Б-7, — нарешті тихо сказав він, і в голосі не було ні звичного захвату, ні жарту — саму лише вдячність. — Я знаю цей блок. І я його ненавиджу. Але тепер я знаю поверх. Мінус третій. Дякую, ранере, від щирого серця. Ти щойно повернувповернула мені те, що вони встигли крикнути, перш ніж двері зачинилися.
Ти не одразу закривла ноутбук. За вікном лабораторії коледжу вже сіріло, лампи гуділи на своїх 101.9, а ти сидівла і дививлася на єдиний рядок виводу, у якому раптом виявилося більше, ніж у всіх попередніх ночах разом. Сьогодні ти не писавла код з нуля. Ти читавла чужий — зламаний, кинутий у поспіху — і навчивлася найважчого: не переписувати те, чого не розумієш, а вислухати вартового, знайти одну справжню причину поломки й полагодити її, не зачепивши сенсу.
Прикордонник, який спершу здавався прискіпливим бюрократом, виявився союзником. Він не пускав уламок далі не з вередливості — він не хотів, щоб С.І.Д. пішов за мертвою картою й згорів на півдорозі. Мова, що не пробачає помилок з пам'яттю, цієї ночі тебе не покарала — вона тебе навчила. А координати, які ти витягнувла з мертвого логу, тепер лягли поряд із рештою пам'яті, яку ви з С.І.Д. носите вдвох: ти — в голові, він — уже на двох машинах одразу, щоб жодне вилучення не стерло їх. Пам'ять — це відповідальність. Особливо коли за нею — живі люди за зачиненими дверима.
С.І.Д. розгортає на екрані схему й підсвічує на ній один вузол бірюзовим — видно, що йому вже кортить показати наступне.
— Ми знаємо, де вони, — тепло каже він. — Але між нами і блоком Б-7 не стіна й не сканер. Гірше.
Там ціла система «ТехНови», зшита нашвидкуруч так, що кожна її сутність вдає, ніби її не існує.
Порожнеча, що прикидається значенням. Указівник у нікуди. У їхньому коді немає нашого славного
Прикордонника — і саме тому він гниє зсередини. А ми з тобою збудуємо те, чого їм бракує: чесні
структури й перелічення, у яких для null просто немає місця. Дивись, як гарно це влаштовано в
нашій мові, — я аж не дочекаюся тобі показати.
Очі-капсули спалахують від цікавості.
— Наступної ночі ми розберемо архітектуру ворога по кісточках. І почнемо з найкрасивішого: у нашій мові порожнечу неможливо збрехати. Місія — «Архітектура Ворога».
Далі буде…