Курс СРС SRS07
до ЛР4 рівень 2 → «4»↔ Лабораторна 4↔ Лекція 4⌨️ команди 3
Самостійна робота · ~2.5 год

Модель володіння Rust проти збирача сміття і ручного керування пам'яттю

🌌 Чому відмичка не ламається — С.І.Д. із задоволенням розкладає все по поличках: у C відмичку можна зламати подвійним звільненням пам'яті, у мовах зі збирачем сміття вона іноді «гальмує» в найгіршу мить — а в Rust, каже він гордо, власник рівно один, і компілятор гарантує це ще до запуску, без жодної паузи в рантаймі. Саме тому він і кує з цієї мови інструменти для дверей Б-7. «Я це давно розкусив», — усміхається голограма над столом.

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

Три моделі керування пам'яттю

Кожна мова програмування має відповісти на питання: коли саме звільняється пам'ять, виділена під значення, і хто за це відповідає. У C/C++ відповідальність повністю лежить на програмісті: malloc/free, new/delete. Це дає максимальний контроль і нульові накладні витрати в рантаймі, але ціна — цілий клас помилок: подвійне звільнення (double free), звернення після звільнення (use-after-free), витік пам'яті (просто забули викликати free). У мовах зі збирачем сміття (Java, Go, Python) ці помилки неможливі за конструкцією — рантайм сам відстежує, на які об'єкти ще є посилання, і звільняє решту. Плата — накладні витрати: періодичні паузи на трасування графа об'єктів (stop-the-world чи конкурентний GC), непередбачувані для систем реального часу, і сам факт роботи фонового потоку/механізму весь час виконання програми.

Rust обирає третій шлях — керування пам'яттю через володіння (ownership), перевірене статично компілятором, без рантайм-накладних витрат і без ризику ручних помилок C-стилю. Базове правило моделі: кожне значення в Rust має рівно одного власника (owner) — змінну, яка за нього відповідає. Коли власник виходить із області видимості (} в кінці блоку), компілятор автоматично вставляє виклик drop для цього значення — і саме тут звільняється пам'ять, закриваються файлові дескриптори, розблоковуються м'ютекси тощо. Це відбувається детерміновано, в точно відомий момент компіляції, а не «колись, коли GC вирішить прибрати».

Друге ключове правило — переміщення (move) замість копіювання за замовчуванням для типів, що володіють ресурсами в купі (наприклад, String, Vec<T>). Коли значення String присвоюється іншій змінній (let s2 = s1;), Rust не копіює дані в купі — він передає володіння: s1 після цього рядка вважається недійсною, і компілятор відхилить будь-яку спробу нею скористатися з помилкою «value used after move» ще на етапі компіляції. Це і є те, що не дозволяє статися double-free: якщо лише один власник відповідає за значення в кожен момент часу, drop викликається рівно один раз, для рівно одного власника.

Важливо розрізняти move і copy: прості типи фіксованого розміру, що повністю лежать на стеку (i32, bool, char, кортежі з таких типів), реалізують трейт Copy — присвоєння таких значень справді копіює біти, і обидві змінні лишаються дійсними. Це не виняток із правила володіння, а окремий, дешевший шлях: копіювання примітива коштує стільки ж, скільки копіювання вказівника, тож заборона користуватися оригіналом після присвоєння не дала б жодної практичної переваги.

Функції, що приймають значення (не посилання), також забирають володіння: передача String у функцію за значенням «переміщує» її туди, і після виклику змінна поза функцією стає недійсною — якщо тільки функція не поверне значення назад. Саме ця жорсткість і компенсується запозиченням (borrowing, тема наступної лабораторної) — механізмом тимчасово «здати в оренду» доступ до значення без передачі володіння. Але вже сама модель володіння — фундамент, без якого запозичення не мало б сенсу: компілятор завжди точно знає, хто відповідає за звільнення кожного шматка пам'яті.

📖 ОПРАЦЮВАТИ

Прочитати розділ «What Is Ownership?» у The Rust Book (rust-lang.org) і спробувати у пісочниці (Rust Playground) приклад із move String; порівняти подумки з тим, як той самий сценарій виглядав би в Java (GC) і в C (malloc/free).

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. У Rust Playground написати функцію, що приймає String за значенням і повертає її довжину; продемонструвати, що після виклику функції змінна поза нею стає недійсною (спробувати вивести її й отримати помилку компіляції, зафіксувати точний текст помилки).
  2. Порівняти поведінку let s2 = s1; для типу String (move) і для типу i32 (copy) — в обох випадках спробувати скористатися s1 після присвоєння й пояснити, чому в одному випадку компіляція падає, а в іншому — ні.
  3. Реалізувати власну структуру з полем String і методом Drop (через impl Drop for ...), що друкує повідомлення при звільненні; створити змінну у внутрішньому блоці {} і показати, що повідомлення з'являється рівно в момент виходу зі скоупу.
  4. Написати невеликий приклад, що імітує подвійне звільнення в стилі C (два вказівники «володіють» тим самим ресурсом), і пояснити письмово, чому еквівалентний код на Rust просто не скомпілюється — знайти конкретне повідомлення компілятора для такої спроби.
  5. Скласти порівняльну таблицю C / Java / Rust за трьома критеріями: хто звільняє пам'ять, коли це відбувається, чи є накладні витрати в рантаймі — і навести по одному класу помилок, характерному для кожної моделі.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чому Rust не потребує збирача сміття в рантаймі, якщо пам'ять усе одно звільняється автоматично?
  2. У який момент компіляції компілятор Rust вставляє виклик drop для значення?
  3. Що відбувається зі змінною s1 після виконання let s2 = s1; для типу String, і чому саме так?
  4. Чим трейт Copy відрізняється від звичайного move-присвоєння, і які типи його реалізують?
  5. Як модель одного власника унеможливлює double-free без участі рантайм-перевірок?
  6. Що станеться, якщо передати String у функцію за значенням, а потім спробувати скористатися оригінальною змінною після виклику?
  7. Яку ціну платить програма з керуванням пам'яттю через збирач сміття порівняно з моделлю володіння Rust?
✅ САМОПЕРЕВІРКА

Чому Rust не потребує збирача сміття в рантаймі, якщо пам'ять усе одно звільняється автоматично — де саме компілятор вставляє виклик звільнення?

🥚 ПАСХАЛКА
Спробуй let serial: u32 = 0xC1D; let copy = serial; — обидві змінні живі, бо u32: Copy. А String::from("C1D") після let b = a; — уже ні: власник рівно один. С.І.Д. каже, що з серійником голопроєктора те саме: число копіюється легко, а шайба на столі — одна, і кому вона належить, Прикордонник знає точно.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 3

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

cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.

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

← SRS06усі темиSRS08 →