Знаєте це відчуття, коли ви беретеся за нову мову і очікуєте, що будете бігти спринт, але замість цього спотикаєтеся на першому ж кроці? Саме це сталося зі мною під час вивчення Rust. У мене була цілком нормальна утиліта на JavaScript — щось, що парсило та змінювало кілька масивів — і я вирішив, що перепишу її за час перерви на каву. Я відкрив редактор, написав код, який здавався мені природним, і запустив компілятор. Він «вибухнув». І не через синтаксичні помилки. І не через відсутність крапок з комою. Він сказав мені, що я намагався використати значення, яке вже було переміщене (moved). Я втупився в екран. Переміщене? Я нічого не переміщував. Я просто присвоїв його іншій змінній.

Зона комфорту JavaScript

JavaScript привчає вас ставитися до даних як до спільної маркерної дошки. Ви оголошуєте об'єкт або масив, передаєте його функції, дозволяєте цій функції додати властивість, а потім передаєте його кудись далі. Збирач сміття (garbage collector) сидить на задньому плані, наче невидимий прибиральник, чекаючи, щоб підмести все, що ви залишите. Ви ніколи не питаєте: «Кому належить цей рядок?». Ви питаєте: «Чи можу я його прочитати?». Якщо відповідь «так», ви з ним працюєте. Якщо вам потрібне ще одне посилання, ви просто пишете let b = a і йдете далі. Обидві змінні вказують на одну й ту саму ділянку пам'яті, і якщо a змінює її, b миттєво бачить ці зміни. Це зручно. Це також хаотично, але цей хаос рідко завдає вам шкоди, оскільки середовище виконання (runtime) бере на себе очищення.

Перший шок від компілятора

Rust вам не довіряє. Це звучить жорстко, але це перше, що ви вивчаєте. Мова побудована навколо системи володіння (ownership) з трьома суворими правилами. По-перше, кожне значення має рівно одного власника. По-друге, в певний момент часу власник може бути лише один. По-третє, коли власник виходить за межі області видимості (scope), Rust автоматично видаляє значення. Жодного збирача сміття. Жодного підрахунку посилань (reference counting) у фоні, якщо ви явно не виберете це. Тільки ці три правила, які контролюються компілятором ще до того, як ваш код почне виконуватися.

Коли я писав свою версію тієї JavaScript-утиліти на Rust, я робив те, що здавалося природним. Я створив вектор (vector) — відповідь Rust на масив — і присвоїв його другій змінній. Потім я спробував використати першу. Компілятор відмовив. У JavaScript let b = a копіює посилання. У Rust це переміщує володіння (moves ownership). Початкова змінна стає недійсною. Компілятор робить це, щоб гарантувати, що у вас ніколи не буде двох шляхів, які випадково наступають на ту саму ділянку пам'яті — саме так у інших системах виникають стан гонитви за даними (data races) та помилки використання пам'яті після звільнення (use-after-free).

Чому Rust забирає ваші іграшки

Погляньте на цей патерн у JavaScript:

let a = [1, 2, 3];
let b = a;
a.push(4);
// Both a and b see 4.

Це вже на рівні інстинкту. Ви б не замислилися над цим. У Rust та сама інтуїція призведе до відмови компілятора ще до того, як ви закінчите друкувати. Як тільки b стає власником вектора, a стає порожнім ім'ям. Ви не можете нічого додати (push) у нього. Ви навіть не можете на нього подивитися. Тепер пам'ять належить b.

Це здається покаранням, поки ви не усвідомите, чого це запобігає. Якби дві змінні могли одночасно змінювати одні й ті самі дані в купі (heap) без координації, ви б ризикували пошкодженням пам'яті. Rust усуває всю цю категорію багів, роблячи передачу даних явною. Компілятор не прискіпливий. Він виконує роль охоронця. Він змушує вас на кожному кроці вирішувати, яка частина вашого коду відповідає за які дані.

Запозичення (Borrowing): обхідний шлях, який насправді є фічею

Звісно, якби кожне присвоєння назавжди передавало володіння, написання програм було б виснажливим. Вам довелося б клонувати все підряд, марнуючи пам'ять і швидкість. Rust вирішує це за допомогою запозичення (borrowing).

Запозичення дозволяє використовувати значення, не забираючи його собі. Існує два типи, і ця різниця є важливою.

  • Незмінні запозичення (immutable borrows), що записуються як &T: Вони дозволяють вам читати дані. Ви можете мати стільки, скільки захочете, одночасно