Управление временем жизни объектов в Rust: секреты сборщика мусора

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

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

Начать курс

Почему «сборщик мусора» в Rust — это не то, что вы думаете

Если вы пришли из мира Python, Java или C#, слово «сборщик мусора» (garbage collector) вызывает чёткий образ: фоновая среда периодически находит и удаляет объекты, на которые больше никто не ссылается. В Rust всё иначе. Здесь нет классического GC, но есть система владения и заимствования, которая решает ту же задачу — управление временем жизни объектов — на этапе компиляции.

Однако жизнь (и код) сложнее идеальных схем. Иногда вам нужно разделить владение одним объектом между несколькими частями программы, создать циклические ссылки или работать с изменяемыми данными через неизменяемые указатели. Именно здесь на сцену выходят garbage-collected объекты в Rust: Rc, Arc, Weak и RefCell. Давайте разберёмся, как управлять их временем жизни, не попасть в утечки памяти и не сломать голову.

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

Ключевые понятия: владение, заимствование и счётчик ссылок

Прежде чем нырять в глубину, вспомним базу:

  • Владение (ownership) — у каждого значения есть ровно один владелец. Когда владелец выходит из области видимости, значение удаляется.
  • Заимствование (borrowing) — вы можете временно «одолжить» ссылку на значение, не становясь владельцем. Ограничения: либо одна изменяемая ссылка, либо сколько угодно неизменяемых.
  • Счётчик ссылок (reference counting) — это механизм, который позволяет иметь несколько владельцев одного значения. В Rust это реализовано через Rc (для однопоточных программ) и Arc (для многопоточных).

Когда мы говорим «garbage-collected объекты» в контексте Rust, мы имеем в виду именно объекты, управляемые счётчиком ссылок. Они не удаляются сразу после выхода из области видимости, а ждут, пока счётчик не упадёт до нуля. Это и есть управление временем жизни — вы контролируете, когда память освободится.

Rc и Arc: основа управления временем жизни

Rc (Reference Counted) — это умный указатель, который позволяет разделять владение одним значением между несколькими частями кода в одном потоке. Каждый раз, когда вы клонируете Rc, счётчик увеличивается. Когда клон выходит из области видимости — уменьшается. Когда счётчик достигает нуля — значение удаляется.

Пример простого использования Rc:

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    let a = Rc::clone(&data);
    let b = Rc::clone(&data);

    println!("Счётчик ссылок: {}", Rc::strong_count(&data)); // 3

    drop(a);
    println!("После удаления a: {}", Rc::strong_count(&data)); // 2
}

Здесь data, a и b — три владельца одного вектора. Пока хотя бы один из них жив, вектор остаётся в памяти. Это идеально для ситуаций, когда вы не можете определить, какой модуль закончит работу последним.

Arc (Atomic Reference Counted) работает так же, но использует атомарные операции, что делает его безопасным для многопоточных программ. Цена — небольшое снижение производительности.

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    let mut handles = vec![];

    for _ in 0..3 {
        let data_clone = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            println!("{:?}", data_clone);
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

Обратите внимание: Arc — это единственный способ безопасно разделить владение между потоками. Если вы попробуете использовать Rc в многопоточном контексте, компилятор выдаст ошибку.

Когда использовать Rc, а когда Arc?

  • Rc — для однопоточных приложений. Быстрее, меньше накладных расходов.
  • Arc — для многопоточных. Безопасность дороже нескольких наносекунд.

Помните: Rc и Arc дают только неизменяемые ссылки на данные. Чтобы изменять значение через счётчик ссылок, нужен RefCell или Mutex.

RefCell: внутренняя изменяемость без боли

RefCell — это контейнер, который реализует внутреннюю изменяемость (interior mutability). Он позволяет изменять данные даже при наличии неизменяемой ссылки на сам контейнер. Как это работает? RefCell проверяет правила заимствования в рантайме, а не на этапе компиляции.

Пример с RefCell внутри Rc:

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared_value = Rc::new(RefCell::new(42));

    let a = Rc::clone(&shared_value);
    let b = Rc::clone(&shared_value);

    // Изменяем значение через a
    *a.borrow_mut() += 10;

    println!("Значение: {}", b.borrow()); // 52
}

Здесь RefCell позволяет нам изменять число, не нарушая правила владения. Если бы мы попытались сделать два изменяемых заимствования одновременно (например, a.borrow_mut() и b.borrow_mut() в одном потоке), программа бы запаниковала в рантайме.

Важно: RefCell — это однопоточный инструмент. Для многопоточных сценариев используйте Mutex или RwLock.

RefCell и время жизни: что нужно знать

Когда вы вызываете borrow() или borrow_mut(), RefCell возвращает умный указатель (Ref или RefMut), который автоматически освобождает блокировку при выходе из области видимости. Это критично: если вы забудете убрать ссылку, получите панику «already borrowed» при повторной попытке заимствования.

fn problem_example() {
    let cell = RefCell::new(10);
    let ref1 = cell.borrow();
    // let ref2 = cell.borrow_mut(); // Ошибка! Уже есть неизменяемая ссылка
    drop(ref1); // Теперь можно
    let ref2 = cell.borrow_mut(); // Ок
}

Управление времен

Блоги

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