Управление памятью в Rust: почему нет сборщика мусора и как это работает

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое сборщик мусора и почему Rust от него отказался?

Если вы пришли из мира Python, Java или C#, то привыкли к тому, что памятью управляет сборщик мусора (Garbage Collector, GC). Это незаметный «уборщик», который периодически проверяет, какие объекты больше не используются, и освобождает занимаемую ими память. Удобно? Да. Но есть нюансы:

  • Непредсказуемые паузы. GC может запуститься в самый неподходящий момент — когда ваш код обрабатывает важный запрос или рендерит кадр в игре.
  • Оверхед по памяти. GC часто требует дополнительной памяти для своей работы.
  • Сложность в real-time системах. В операционных системах, драйверах или встроенных устройствах паузы GC неприемлемы.

Rust предлагает радикально иной подход. Вместо сборщика мусора используется система владения (ownership), которая работает на этапе компиляции. Компилятор сам решает, когда память должна быть освобождена, и делает это без какого-либо runtime-оверхеда. Давайте разберемся, как это работает.

Система владения (Ownership) — основа управления памятью

В Rust у каждого значения есть ровно один владелец — переменная, которой это значение принадлежит. Когда владелец выходит из области видимости (scope), память автоматически освобождается. Никакого сборщика мусора, никаких подсчетов ссылок — только компилятор и строгие правила.

Правила владения в Rust

Вот три главных правила, которые компилятор проверяет на этапе компиляции:

  • У каждого значения в Rust есть один владелец.
  • Когда владелец выходит из области видимости, значение удаляется (drop).
  • Значение можно передать другому владельцу (move) или одолжить (borrow).
fn main() {
    let s1 = String::from("Hello"); // s1 — владелец строки
    let s2 = s1;                     // Владение ПЕРЕХОДИТ к s2 (move)
    // println!("{}", s1);           // Ошибка! s1 больше не владеет строкой
    println!("{}", s2);             // Ok: s2 — новый владелец
} // Здесь и s1, и s2 выходят из области видимости, 
  // но память освобождается только один раз — для s2

В Python или Java вы бы просто создали еще одну ссылку на объект, и GC разобрался бы с ней позже. В Rust — никаких сюрпризов: компилятор гарантирует, что память будет освобождена ровно один раз.

Заимствование (Borrowing) — как использовать данные без передачи владения

Часто нам нужно временно «одолжить» доступ к данным, не забирая их у владельца. Для этого в Rust есть ссылки (references). Они бывают двух типов:

  • Неизменяемые ссылки (&T): можно читать данные, но нельзя изменять.
  • Изменяемые ссылки (&mut T): можно читать и изменять, но только если нет других активных ссылок.
fn main() {
    let mut text = String::from("Rust");
    
    let r1 = &text;     // неизменяемая ссылка
    let r2 = &text;     // еще одна неизменяемая — можно
    println!("{} {}", r1, r2);
    
    let r3 = &mut text; // изменяемая ссылка — работает, т.к. r1 и r2 больше не используются
    r3.push_str(" rocks!");
    println!("{}", r3);
    
    // А вот так нельзя:
    // let r1 = &text;
    // let r3 = &mut text; // Ошибка! Нельзя иметь изменяемую и неизменяемую ссылки одновременно
}

Компилятор Rust — это не просто статический анализатор, это строгий, но справедливый учитель. Он не даст вам выстрелить себе в ногу. Если вы попытаетесь создать изменяемую ссылку, пока есть неизменяемые, — получите ошибку компиляции. Это и есть работа borrow checker — механизма, который заменяет сборщик мусора.

Время жизни (Lifetimes) — как долго живут ссылки?

Иногда компилятору нужно понять, как долго ссылка будет действительна. Для этого используются аннотации времени жизни (lifetime annotations). Они выглядят как 'a, 'b и т.д. и указывают, что ссылки должны жить не дольше, чем данные, на которые они указывают.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("длинная строка");
    let result;
    {
        let string2 = String::from("короткая");
        result = longest(&string1, &string2); // Ошибка! string2 живет меньше, чем result
    }
    // println!("{}", result); // Раскомментируйте — и получите ошибку компиляции
}

Аннотации времени жизни — это не runtime-проверка, а подсказка компилятору. Он использует их, чтобы убедиться: ссылка не переживет данные, на которые она указывает. В Python вы бы получили ошибку только во время выполнения (или вообще не получили бы, если объект еще жив благодаря GC). В Rust — ошибка на этапе компиляции.

RAII — Resource Acquisition Is Initialization (и это не про сборщик мусора)

Rust использует идиому RAII, которая пришла из C++. Суть проста: ресурс (память, файловый дескриптор, сокет) захватывается при инициализации объекта и освобождается при его уничтожении. Когда переменная выходит из области видимости, вызывается деструктор (drop), который освобождает ресурс.

use std::fs::File;
use std::io::Write;

fn main() {
    let mut file = File::create("example.txt").unwrap(); // Файл открыт
    file.write_all(b"Hello, RAII!").unwrap();
    // Файл автоматически закрывается, когда file выходит из области видимости
} // Здесь вызывается drop для file — файл закрывается

В Python вы бы написали with open(...) as f: или забыли бы закрыть файл, надеясь на GC. В Rust файл закроется автоматически в конце блока. Никаких finally, никакого GC — просто строгие правила компилятора.

А как же умные указатели? Box, Rc, Arc

Иногда нужно разместить данные в куче (heap) или разделить владение между несколькими частями программы. Для этого Rust предоставляет умные указатели:

  • Box<T>

Блоги

Рекомендаций по книгам