← Назад к списку тем

12. Умные указатели

Box, Rc/Arc, RefCell/Cell, Weak, Deref/DerefMut, Drop, интерьерная изменяемость, циклы ссылок.

Box<T> — единоличное владение в куче

Box<T> — простейший умный указатель: аллоцирует T в куче, а на стеке хранит только указатель. Никакого рантайм-оверхеда — только одна аллокация/деаллокация. Нужен, когда размер типа неизвестен на этапе компиляции (рекурсивные типы) или когда нужно передать владение большим значением без копирования.

// Рекурсивный тип без Box не компилируется — размер бесконечен
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

// dyn Trait — размер неизвестен, нужен Box для хранения в структуре/Vec
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle), Box::new(Square)];

Box реализует Deref и DerefMut, поэтому к содержимому можно обращаться так же, как к обычной ссылке — это называют deref coercion: компилятор автоматически применяет * при вызове методов.

Rc<T> vs Arc<T> — совместное владение

Rc (Reference Counted) и Arc (Atomically Reference Counted) дают несколько владельцев одним данным. При клонировании копируется не сам объект, а указатель, а счётчик ссылок увеличивается; данные удаляются, когда счётчик достигает нуля. Разница — единственная, но критичная: механизм подсчёта ссылок.

use std::rc::Rc;

let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a);   // увеличивает strong_count, данные не копируются
println!("{}", Rc::strong_count(&a)); // 2

// Rc НЕ реализует Send/Sync — компилятор не даст передать его в поток
// std::thread::spawn(move || { let _ = a; }); // ошибка компиляции

Rc увеличивает счётчик обычной (не атомарной) операцией +1/-1 — быстро, но небезопасно при доступе из нескольких потоков одновременно (data race на самом счётчике). Поэтому компилятор запрещает передавать Rc между потоками через отсутствие Send. Arc использует атомарные инструкции CPU для инкремента/декремента счётчика — дороже, но безопасно конкурентно.

Rc<T>Arc<T>
Потокобезопасностьнет (single-thread)да (atomic operations)
Send / Syncне реализуетреализует (если T: Send + Sync)
Стоимость clone()обычный инкрементatomic fetch_add
Когда использоватьграф/дерево в одном потокеданные, шарящиеся между потоками
⚠️ Не берите Arc "на всякий случай": в однопоточном коде Arc просто платит за атомарные операции без выгоды. Компилятор всё равно не позволит использовать Rc там, где нужна многопоточность — так что выбор диктуется требованием, а не привычкой.

Cell и RefCell — интерьерная изменяемость

Правило "либо одна mut-ссылка, либо много immutable" обычно проверяется компилятором статически. Но иногда нужно изменить данные через immutable-ссылку (например, поле внутри Rc, у которого нет &mut доступа). Для этого существует interior mutability — паттерн, при котором проверка правил заимствования переносится с compile-time на runtime.

use std::cell::{Cell, RefCell};

// Cell<T> — для Copy-типов, работает через get/set, без ссылок вообще
struct Counter { count: Cell<i32> }
let c = Counter { count: Cell::new(0) };
c.count.set(c.count.get() + 1);   // нет заимствований — просто copy in/out

// RefCell<T> — для любых типов, borrow-checking переносится в runtime
let shared = RefCell::new(vec![1, 2, 3]);
{
    let mut m = shared.borrow_mut();  // runtime-проверка: единственный mut borrow
    m.push(4);
}   // borrow освобождается здесь (Drop у RefMut)
println!("{:?}", shared.borrow());

RefCell хранит внутри флаг состояния заимствования (счётчик активных borrow()/borrow_mut()). Если правила нарушены — не ошибка компиляции, а panic в runtime:

let cell = RefCell::new(5);
let _b1 = cell.borrow_mut();
let _b2 = cell.borrow_mut();  // panic: already mutably borrowed: BorrowMutError
🚫 Ключевая опасность: компилятор здесь бессилен — RefCell откладывает проверку заимствований до выполнения. Двойной borrow_mut() в одной области видимости — это panic в production, а не ошибка сборки. Используйте try_borrow()/try_borrow_mut(), если нужно обработать конфликт без паники.

Rc<RefCell<T>> — комбинация для изменяемых графов

Rc сам по себе даёт только immutable-доступ (клонирование не даёт &mut). Комбинация Rc<RefCell<T>> — стандартный в Rust способ получить "несколько владельцев + изменяемость" в однопоточном коде — аналог того, что в Go или Python получается "бесплатно" через ссылочную семантику объектов.

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

struct Node {
    value: i32,
    children: Vec<Rc<RefCell<Node>>>,
}

let leaf = Rc::new(RefCell::new(Node { value: 3, children: vec![] }));
let branch = Rc::new(RefCell::new(Node { value: 5, children: vec![Rc::clone(&leaf)] }));

leaf.borrow_mut().value = 10;   // изменение через immutable Rc
println!("{}", branch.borrow().children[0].borrow().value); // 10

Циклы ссылок и Weak<T>

Rc не защищает от утечек памяти: если два узла ссылаются друг на друга через Rc, счётчик ссылок никогда не дойдёт до нуля, даже когда объекты снаружи недостижимы — классическая reference cycle. Rust не имеет трассирующего GC, который мог бы это обнаружить, поэтому ответственность за разрыв циклов лежит на разработчике.

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

struct Node {
    value: i32,
    parent: RefCell<Option<Rc<Node>>>,   // сильная ссылка вверх → цикл!
    children: RefCell<Vec<Rc<Node>>>,
}
// parent (Rc) держит child живым через children,
// child (Rc) держит parent живым через parent → утечка навсегда

Решение — заменить одно из направлений (обычно "родитель") на Weak<T>: слабую ссылку, которая не увеличивает strong_count и не мешает удалению данных.

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,       // слабая ссылка — не мешает Drop
    children: RefCell<Vec<Rc<Node>>>,
}

let leaf = Rc::new(Node {
    value: 3,
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
    value: 5,
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);  // Weak, не Rc

// Weak нужно "апгрейдить" в Rc перед использованием — данные могли уже удалиться
if let Some(parent) = leaf.parent.borrow().upgrade() {
    println!("parent = {}", parent.value);
}
Rc<T>Weak<T>
Влияет на strong_countданет
Гарантирует существование данныхданет, нужен .upgrade()
Типичное применениеowning-связь (parent → children)back-reference (child → parent)
🔑 Ключевое: направление владения в графе должно быть деревом сильных ссылок (Rc) с необязательными обратными слабыми ссылками (Weak) — так counting-based GC остаётся детерминированным.

Deref, DerefMut и Drop

Умные указатели ведут себя как обычные ссылки благодаря трейтам Deref/DerefMut — они позволяют компилятору применять deref coercion: автоматически преобразовывать &MyBox<String> в &str при вызове функций.

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target { &self.0 }
}

fn greet(name: &str) { println!("Hello, {name}"); }

let b = MyBox(String::from("Rust"));
greet(&b);  // &MyBox<String> → &String → &str, всё автоматически

Drop вызывается автоматически при выходе значения из области видимости — это основа RAII в Rust. Для умных указателей с ресурсами (файлы, соединения, кастомные аллокации) кастомный Drop гарантирует освобождение без сборщика мусора.

struct Guard { name: String }

impl Drop for Guard {
    fn drop(&mut self) {
        println!("Освобождаю {}", self.name);
    }
}
// Drop нельзя вызвать вручную (guard.drop() — ошибка компиляции)
// Явный вызов — только через std::mem::drop(guard)

Практический выбор указателя

На практике выбор сводится к нескольким вопросам: нужен ли один владелец или несколько, нужна ли мутация через immutable-ссылку, нужна ли многопоточность.

ТипВладениеИзменяемостьПотоки
Box<T>единоличноеобычные правилаSend, если T: Send
Rc<T>совместноетолько через RefCellнет
Arc<T>совместноетолько через Mutex/RwLockда
RefCell<T>runtime-checkedнет (не Sync)
Mutex<T>через блокировкуда

Типичная эволюция при переходе от однопоточного кода к многопоточному: Rc<RefCell<T>>Arc<Mutex<T>>. Компилятор сам укажет на это несоответствие ошибкой "T cannot be sent between threads safely", если попытаться распараллелить код с Rc.