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 (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 просто платит за атомарные операции без выгоды. Компилятор всё равно не позволит использовать Rc там, где нужна многопоточность — так что выбор диктуется требованием, а не привычкой.
Правило "либо одна 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 сам по себе даёт только 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
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 — они позволяют компилятору применять 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.