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.