Avatar ·

Что в Rust используется вместо сборщика мусора для управления памятью?

Привет! Я только начал изучать Rust после Java и Python, и у меня возникло недопонимание. В Java и Python есть сборщик мусора (GC), который автоматически освобождает память, когда объекты больше не нужны. Но я читал, что в Rust GC нет. Как тогда Rust управляет памятью без сборщика мусора? Неужели мне придётся вручную вызывать `free()` или `delete`, как в C? Это ведь неудобно и чревато утечками! Вот простой пример, который я написал, пытаясь понять, что произойдёт: ```rust fn main() { let s1 = String::from("Привет"); let s2 = s1; // Я ожидал копию, как в Java println!("{}", s1); // Это не компилируется! Ошибка: borrow of moved value } ``` Я не понимаю, почему `s1` больше недоступен после присваивания `s2`. В Java `s2` просто указывала бы на ту же строку, а `s1` осталась бы валидной. Что такое "move" и как это связано с отсутствием GC? И как Rust гарантирует безопасность памяти без сборщика мусора? Объясните, пожалуйста, для новичка. Также я слышал про "владение" и "заимствование", но не могу связать это с управлением памятью. Помогите разобраться! <<>>

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

В Rust нет сборщика мусора, потому что он использует систему владения (ownership) с набором правил, проверяемых на этапе компиляции. Это позволяет автоматически освобождать память без накладных расходов GC и без ручного управления, как в C. Ваш пример как раз демонстрирует ключевое правило: у каждого значения в Rust ровно один владелец.

Когда вы пишете let s2 = s1;, происходит не копирование данных, а перемещение (move) владения. Переменная s1 передаёт владение строкой s2 и становится недействительной. Компилятор запрещает использовать s1 после перемещения — это предотвращает ситуации с "висячими указателями" и двойным освобождением памяти, которые возможны в C++.

fn main() {
    let s1 = String::from("Привет");
    let s2 = s1; // s1 перемещается в s2
    // println!("{}", s1); // Ошибка! s1 больше не владеет строкой
    println!("{}", s2); // Работает: s2 — новый владелец
}

Как освобождается память?

Когда переменная-владелец выходит из области видимости, Rust автоматически вызывает деструктор (drop) и освобождает память. Это называется Resource Acquisition Is Initialization (RAII), как в C++. В Java и Python это делает GC, но здесь всё детерминировано и происходит сразу в конце блока.

fn main() {
    {
        let s = String::from("временная строка");
        // Здесь s используется
    } // s выходит из области видимости — память освобождена
    // println!("{}", s); // Ошибка: s больше не существует
}

Что, если нужно копирование?

Для типов, которые хранятся на стеке (например, числа, булевы значения), работает копирование через трейт Copy. Для типов в куче (как String) нужно явно вызывать .clone():

fn main() {
    let s1 = String::from("Привет");
    let s2 = s1.clone(); // Глубокое копирование
    println!("{} {}", s1, s2); // Теперь обе переменные валидны
}

Заимствование (Borrowing) и ссылки

Если нужно временно использовать данные без передачи владения, применяются ссылки (заимствование). Ссылки не становятся владельцами, поэтому исходная переменная остаётся доступной:

fn main() {
    let s1 = String::from("Привет");
    let len = calculate_length(&s1); // &s1 — ссылка, заимствование
    println!("Длина '{}' = {}", s1, len); // s1 всё ещё доступна
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // s выходит из области видимости, но так как это ссылка, память не освобождается

Компилятор Rust проверяет правила заимствования:

  • В любой момент может быть либо одна изменяемая ссылка, либо сколько угодно неизменяемых.
  • Ссылки всегда должны быть валидными (нельзя вернуть ссылку на локальную переменную).

Итог

Rust заменяет сборщик мусора системой владения, заимствования и времён жизни (lifetimes). Все проверки выполняются на этапе компиляции, поэтому программа работает без накладных расходов на GC и без риска утечек памяти (если не использовать unsafe). Это даёт производительность, сравнимую с C/C++, но с гарантиями безопасности памяти.

<<>>

Дополнение: времена жизни (lifetimes) и умные указатели

Первый ответ отлично объяснил основы. Хочу добавить, что полное управление памятью в Rust включает также времена жизни (lifetimes) и умные указатели для сложных сценариев.

Времена жизни — это аннотации, которые помогают компилятору проверить, что ссылки не переживают данные, на которые они указывают. Обычно они неявные, но в сложных функциях приходится указывать явно:

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

Для случаев, когда нужно разделяемое владение (как в Java), Rust предоставляет умные указатели:

  • Rc<T> — подсчёт ссылок для одного потока.
  • Arc<T> — атомарный подсчёт ссылок для многопоточности.
  • RefCell<T> — внутренняя изменяемость с проверкой на этапе выполнения.

Эти конструкции имитируют поведение GC, но с явным контролем. Например, Rc освобождает память, когда счётчик ссылок падает до нуля:

use std::rc::Rc;

fn main() {
    let s = Rc::new(String::from("Привет"));
    let s2 = Rc::clone(&s); // Увеличивает счётчик
    println!("{}", s); // Оба владельца валидны
} // Когда все Rc выходят из области видимости, память освобождается

Таким образом, Rust не нуждается в полноценном GC, потому что его система типов и правила владения покрывают 99% случаев управления памятью, а для оставшихся 1% есть умные указатели. Это даёт предсказуемую производительность и нулевые накладные расходы на сборку мусора.