Чем Rust заменяет сборщик мусора? Альтернативы GC в Rust
Владение (Ownership): главный механизм управления памятью в Rust
В Rust нет сборщика мусора, потому что он использует систему владения (ownership) с набором строгих правил, проверяемых на этапе компиляции. Это ключевая идея: компилятор сам анализирует, когда память должна быть освобождена, без необходимости в рантайме.
Три правила владения
- Каждое значение в Rust имеет одного владельца (переменную, которая его хранит).
- Владелец может быть только один в любой момент времени.
- Когда владелец выходит из области видимости, значение удаляется, а память освобождается.
Разбор вашего примера
В вашем коде переменная user является владельцем структуры User. Когда вы вызываете process(user), происходит перемещение (move) владения: теперь владельцем становится параметр u внутри функции. После вызова user больше не владеет данными — они "переехали" в функцию.
fn main() {
let user = User { ... }; // user — владелец
process(user); // владение перемещается в process
// user больше недействителен!
// println!("{}", user.name); // Ошибка компиляции
}
fn process(u: User) { // u становится новым владельцем
println!("Processing: {}", u.name);
// Здесь u выходит из области видимости, память освобождается
} // Автоматически вызывается drop() для u
Когда функция process завершается, её параметр u выходит из области видимости, и Rust автоматически вызывает drop() — аналог деструктора. Память освобождается сразу, без ожидания сборщика мусора. Это называется RAII (Resource Acquisition Is Initialization): ресурс освобождается при уничтожении объекта.
Альтернативы: заимствование (borrowing) вместо перемещения
Если вы хотите использовать данные после передачи в функцию, нужно не перемещать владение, а заимствовать их с помощью ссылок:
fn main() {
let user = User { ... };
process(&user); // передаём ссылку (заимствование)
println!("{}", user.name); // Работает! user всё ещё владелец
}
fn process(u: &User) { // принимаем ссылку
println!("Processing: {}", u.name);
// u — ссылка, владение не перемещается
} // Ссылка u уничтожается, но данные user остаются
Ссылки бывают двух типов:
- Неизменяемые (&T) — можно сколько угодно одновременно читать.
- Изменяемые (&mut T) — только одна ссылка в данный момент, гарантия отсутствия гонок данных.
Что насчёт динамических структур?
Для данных, размер которых неизвестен на этапе компиляции (например, строки, векторы), Rust использует кучу (heap). Но освобождение всё равно управляется владением: когда владелец выходит из области видимости, вызывается drop(), который освобождает и память на куче.
fn main() {
let s = String::from("hello"); // s — владелец строки на куче
// ... используем s
} // s выходит из области видимости -> drop() -> память освобождена
Итог: почему это лучше GC?
- Предсказуемость: память освобождается сразу, нет "стоп-мир" пауз.
- Безопасность: все проверки на этапе компиляции, нет утечек или двойного освобождения.
- Производительность: нет накладных расходов на сборщик мусора в рантайме.
Дополнение: Rc и Arc — разделённое владение без GC
В Rust есть ситуации, когда одно значение должно иметь несколько владельцев (например, графы или разделяемые данные). Для этого существуют умные указатели Rc (однопоточный счётчик ссылок) и Arc (многопоточный, атомарный).
use std::rc::Rc;
fn main() {
let data = Rc::new("Hello".to_string());
let a = Rc::clone(&data); // счётчик ссылок = 2
let b = Rc::clone(&data); // счётчик = 3
println!("{}", a); // все три переменные владеют данными
println!("{}", b);
} // при выходе из области видимости счётчик уменьшается
// когда счётчик достигает 0, память освобождается
Это похоже на ссылочное подсчитывание в Python или Swift, но с ключевым отличием: циклические ссылки могут привести к утечкам памяти, так как счётчик никогда не обнулится. Для циклических ссылок в Rust используют Weak — слабые ссылки, которые не увеличивают счётчик.
Таким образом, Rust не отказывается от счётчика ссылок полностью, но предлагает его как опцию для особых случаев, а не как единственный механизм управления памятью. Основной же подход — статическая проверка владения на этапе компиляции, что даёт максимальную производительность и безопасность.
Важно: Rc и Arc — это библиотечные типы, а не встроенные в язык. Они используются только когда действительно нужно множественное владение. В 95% случаев достаточно обычного владения и заимствования.