RustMiddleTechnical
Что такое Box<T>, Rc<T>, Arc<T> и RefCell<T>? Когда использовать каждый из них?
Box<T> — единственный owner на heap; Rc<T> — ref-counted sharing в одном потоке; Arc<T> — то же для нескольких потоков (атомарный счётчик); RefCell<T> — interior mutability с runtim borrow checking.
Box<T> — единственный владелец, данные на heap
// Box<T> — простейший smart pointer: единственный owner, данные на heap
let b = Box::new(5i32); // 5 на heap, b на stack
println!("b = {b}"); // auto-deref
drop(b); // heap-память освобождается
// Основные use-cases:
// 1. Рекурсивные типы
enum Tree {
Leaf(i32),
Node(Box<Tree>, Box<Tree>), // без Box — бесконечный размер
}
// 2. Trait objects
fn make_greeting(formal: bool) -> Box<dyn std::fmt::Display> {
if formal {
Box::new(String::from("Добрый день"))
} else {
Box::new(42i32) // оба Display
}
}
Rc<T> — reference counting для однопоточного кода
use std::rc::Rc;
// Rc позволяет нескольким владельцам разделять одни данные
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // clone увеличивает счётчик, не копирует данные
let c = Rc::clone(&a);
println!("Count: {}", Rc::strong_count(&a)); // 3
// Когда последний Rc дропается — данные освобождаются
drop(b);
println!("Count: {}", Rc::strong_count(&a)); // 2
// НЕ Send — нельзя передать в другой поток!
// Rc использует non-atomic счётчик
Arc<T> — atomically reference counted для многопоточности
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let handles: Vec<_> = (0..4).map(|i| {
let data = Arc::clone(&data); // clone безопасен из нескольких потоков
thread::spawn(move || {
println!("Thread {i}: {:?}", data);
})
}).collect();
for h in handles { h.join().unwrap(); }
// Arc<Mutex<T>> — стандартный паттерн для shared mutable state
use std::sync::Mutex;
let counter = Arc::new(Mutex::new(0i32));
let counter_clone = Arc::clone(&counter);
thread::spawn(move || {
*counter_clone.lock().unwrap() += 1;
}).join().unwrap();
println!("Counter: {}", *counter.lock().unwrap());
RefCell<T> — interior mutability в однопоточном коде
use std::cell::RefCell;
// RefCell позволяет изменять данные через &T (shared reference)
// Borrow checking перенесён в рантайм
let data = RefCell::new(vec![1, 2, 3]);
// Immutable borrow
{
let r = data.borrow(); // BorrowRef
println!("{:?}", *r);
} // r дропается
// Mutable borrow
{
let mut w = data.borrow_mut(); // BorrowRefMut
w.push(4);
} // w дропается
// PANIC если правила нарушены в рантайме:
// let _r = data.borrow();
// let _w = data.borrow_mut(); // thread 'main' panicked: already borrowed
// Типичный паттерн: Rc<RefCell<T>> для shared mutable state в однопоточном коде
let shared = Rc::new(RefCell::new(0i32));
let clone1 = Rc::clone(&shared);
*clone1.borrow_mut() += 10;
println!("{}", *shared.borrow());
Когда что использовать — таблица решений
- Box<T>: единственный owner, heap-аллокация, рекурсивные типы, trait objects.
- Rc<T>: несколько owners, один поток, read-only sharing.
- Arc<T>: несколько owners, несколько потоков, thread-safe sharing.
- RefCell<T>: interior mutability в одном потоке, runtim borrow checks.
- Arc<Mutex<T>>: shared mutable state между потоками.
- Arc<RwLock<T>>: shared state где читают часто, пишут редко.
Подводные камни
- Rc
> может создавать циклы: A→B→A — память не освобождается. Используйте Weak для обратных ссылок. - Arc добавляет overhead на атомарные операции: при высокой contention хуже, чем локальные структуры.
- RefCell panic в рантайме если нарушить borrow rules — тестируйте edge cases.
- Box
требует vtable lookup на каждый вызов — это динамическая диспетчеризация, не нулевой overhead. - Mutex в async-коде должен быть tokio::sync::Mutex, не std::sync::Mutex — иначе блокирует worker thread.
- Rc не Send — компилятор не даст передать Rc в thread::spawn, но код с Arc
> скомпилируется и даст UB через unsafe. - Слишком глубокая вложенность: Arc
>>> сигнализирует о проблеме архитектуры. - Weak::upgrade() возвращает Option
> — нужно обрабатывать случай, когда strong count уже 0.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.