🌌 Чому відмичка не ламається — С.І.Д. із задоволенням розкладає все по поличках: у 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).
Чому Rust не потребує збирача сміття в рантаймі, якщо пам'ять усе одно звільняється автоматично — де саме компілятор вставляє виклик звільнення?
let serial: u32 = 0xC1D; let copy = serial; — обидві змінні живі, бо u32: Copy. А String::from("C1D") після let b = a; — уже ні: власник рівно один. С.І.Д. каже, що з серійником голопроєктора те саме: число копіюється легко, а шайба на столі — одна, і кому вона належить, Прикордонник знає точно.Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
cargo run-- йдуть програмі (cargo run -- staff.log).cargocargo buildtarget/debug/; --release — з оптимізаціями у target/release/.💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.