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.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics