Box, Rc, RefCell и внутренняя изменяемость

Умные указатели нужны там, где статических правил заимствования не хватает: куча, несколько владельцев, изменение через общую ссылку.

Interior mutability — возможность изменить содержимое объекта, имея на руках только разделяемую ссылку &T, за счёт переноса проверки заимствований в рантайм.

Вопрос: «Зачем нужен Box и когда он обязателен?»

Что на самом деле проверяет интервьюер

Понимаете ли вы, что Box<T> — это не «объект в куче ради кучи», а indirection: указатель известного размера вместо значения неизвестного или неудобного размера. Три канонических случая: рекурсивные типы, трейт-объекты, перенос большого значения без копирования байтов.

enum List {
    Cons(i32, List),
    Nil,
}

fn main() {
    let _l = List::Cons(1, List::Cons(2, List::Nil));
}

Ошибка компиляции:

error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

Размер List считается как «i32 плюс размер List» и уходит в бесконечность. Box обрывает цепочку: размер варианта становится 4 + 8 байт.

enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn sum(list: &List) -> i32 {
    match list {
        List::Cons(v, rest) => v + sum(rest),
        List::Nil => 0,
    }
}

fn main() {
    let l = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    println!("{}", sum(&l));
}

Вывод:

3

Второй обязательный случай вы уже видели в разделе про трейты: Vec<Box<dyn Draw>> — единственный способ сложить в один вектор разнотипные реализации, потому что dyn Draw не имеет размера, известного на этапе компиляции.

Вопрос: «Что такое Rc и когда он нужен?»

Что на самом деле проверяет интервьюер

Знаете ли вы, что владелец у значения обычно один, и Rc<T> — способ легально нарушить это правило в пределах одного потока: узел графа, разделяемая таблица конфигурации, кэш, на который ссылаются несколько структур.

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    println!("{}", Rc::strong_count(&data));
    let a = Rc::clone(&data);
    {
        let _b = Rc::clone(&data);
        println!("{}", Rc::strong_count(&data));
    }
    println!("{}", Rc::strong_count(&data));
    println!("{:?}", a);
}

Вывод:

1
3
2
[1, 2, 3]

Rc::clone не копирует данные — он увеличивает счётчик, поэтому дешёвый; пишут именно Rc::clone(&x), а не x.clone(), чтобы на ревью было видно, что это разделение, а не глубокая копия. Данные освобождаются, когда счётчик доходит до нуля. И главное ограничение: через Rc доступно только чтение.

use std::rc::Rc;

fn main() {
    let shared = Rc::new(vec![1, 2, 3]);
    shared.push(4);
}

Ошибка компиляции:

error[E0596]: cannot borrow data in an `Rc` as mutable
 --> src/main.rs:5:5
  |
5 |     shared.push(4);
  |     ^^^^^^ cannot borrow as mutable
  |
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Vec<i32>>`

Вопрос: «Что такое interior mutability и чем RefCell отличается от &mut?»

Что на самом деле проверяет интервьюер

Не решили ли вы, что RefCell отключает borrow checker. Правила остались те же — много читателей либо один писатель, — но проверяются они в рантайме, и цена нарушения не ошибка компиляции, а паника.

use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(vec![1, 2, 3]);
    let first = cell.borrow();
    cell.borrow_mut().push(4);
    println!("{:?}", first);
}

Вывод:

thread 'main' panicked at src/main.rs:6:10:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Компилятор такой код пропускает: с его точки зрения cell заимствован разделяемо оба раза. Конфликт обнаруживает сам RefCell, храня счётчики активных заимствований. Поэтому держать Ref/RefMut дольше одного выражения — плохая привычка.

Классическая связка Rc<RefCell<T>>

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

#[derive(Debug)]
struct Counter {
    hits: u32,
}

fn main() {
    let shared = Rc::new(RefCell::new(Counter { hits: 0 }));
    let logger = Rc::clone(&shared);

    logger.borrow_mut().hits += 1;
    shared.borrow_mut().hits += 1;

    println!("{}", shared.borrow().hits);
    println!("{}", Rc::strong_count(&shared));
}

Вывод:

2
2

Rc даёт нескольких владельцев, RefCell — право изменять. Плата за это — риск цикла: если два узла ссылаются друг на друга через Rc, счётчики никогда не дойдут до нуля и память утечёт. Разрывают цикл через Weak: слабая ссылка не удерживает данные, а upgrade() возвращает Option<Rc<T>>.

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

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

Для простых Copy-типов есть более дешёвый Cell<T>: он не раздаёт ссылок внутрь, а только копирует значение целиком через get/set, поэтому и паниковать ему не на чем.

use std::cell::Cell;

struct Stats {
    calls: Cell<u32>,
}

impl Stats {
    fn touch(&self) {
        self.calls.set(self.calls.get() + 1);
    }
}

Памятка

ТипВладельцевКогда проверкаПотоки
Box<T>одинкомпиляцияSend/Sync, если T такой
Rc<T>многокомпиляция (только чтение)нет, один поток
Arc<T>многокомпиляция (только чтение)да, атомарный счётчик
RefCell<T>одинрантайм, паниканет, не Sync
Mutex<T>одинрантайм, блокировкада

Типичные ошибки кандидатов

  • Считать Box «аналогом new из C++» и не называть главные случаи: рекурсивный тип, трейт-объект, большое значение.
  • Думать, что Rc::clone копирует данные, — на самом деле он лишь увеличивает счётчик.
  • Говорить, что RefCell «отключает borrow checker»: он его переносит в рантайм, правила остаются.
  • Не помнить, что Rc и RefCell однопоточные, и предлагать их вместо Arc и Mutex.
  • Забывать про цикл Rc и утечку памяти, не упоминать Weak.
  • Держать результат borrow() в переменной на пол-функции и удивляться BorrowMutError.
  • Тянуть Rc<RefCell<T>> туда, где хватило бы обычного &mut или передачи владения.

Как ответить кратко

Box<T> — владеющий указатель на кучу; обязателен, когда размер типа иначе бесконечен (рекурсивные структуры, ошибка E0072) или неизвестен (dyn Trait). Rc<T> — счётчик ссылок для нескольких владельцев в одном потоке: Rc::clone лишь инкрементирует strong_count, а доступ даётся только на чтение. RefCell<T> — это interior mutability: те же правила заимствования, но проверка в рантайме и паника BorrowMutError при нарушении. Комбинация Rc<RefCell<T>> даёт разделяемое изменяемое состояние ценой риска циклов, который разрывают через Weak. Для многопоточности те же роли играют Arc и Mutex.

Проверьте себя
1. Почему рекурсивный enum вида `enum List { Cons(i32, List), Nil }` не компилируется?
ARust запрещает рекурсивные типы данных в принципе
BКомпилятор не может вычислить конечный размер типа — ошибка E0072
CНужен явный лайфтайм для варианта Cons
DТакой enum требует атрибута #[repr(C)]
2. Что делает Rc::clone?
AГлубоко копирует данные в новую область кучи
BУвеличивает счётчик сильных ссылок и возвращает второй владеющий указатель
CПревращает Rc в Arc для передачи в другой поток
DСоздаёт слабую ссылку, не мешающую освобождению данных
3. Чем RefCell отличается от обычного &mut?
ARefCell отключает правила заимствования полностью
BRefCell проверяет правила заимствования в рантайме и паникует при нарушении
CRefCell делает тип потокобезопасным
DRefCell хранит данные в куче, а &mut — на стеке