Что такое interior mutability в Rust? Как Cell<T> и RefCell<T> её обеспечивают?
Interior mutability — изменение данных через разделяемые (&T) ссылки. Cell<T> даёт Copy-семантику без блокировок, RefCell<T> — динамическую проверку borrow rules с возможностью получить &mut T во время выполнения.
Interior Mutability в Rust
По умолчанию Rust запрещает изменение значения через &T. Interior mutability — паттерн, нарушающий это правило безопасным или явно небезопасным способом. Он реализован через типы из std::cell и примитивы синхронизации.
Cell<T>
Cell<T> работает только с типами, реализующими Copy. Метод get() копирует значение, set() — заменяет. Нет ни указателей внутрь, ни блокировок, нет проверок во время выполнения — всё сводится к обычным операциям чтения/записи.
use std::cell::Cell;
struct Counter {
value: Cell<u32>,
}
impl Counter {
fn new() -> Self {
Counter { value: Cell::new(0) }
}
// Принимает &self, но изменяет внутреннее состояние
fn increment(&self) {
self.value.set(self.value.get() + 1);
}
fn get(&self) -> u32 {
self.value.get()
}
}
fn main() {
let c = Counter::new();
c.increment();
c.increment();
println!("{}", c.get()); // 2
}
RefCell<T>
RefCell<T> работает с любыми типами, проверяет правила заимствования динамически (в runtime) и паникует при нарушении. borrow() возвращает Ref<T> (аналог &T), borrow_mut() — RefMut<T> (аналог &mut T). Одновременно допускается любое число Ref, но не более одного RefMut и без одновременных Ref.
use std::cell::RefCell;
use std::rc::Rc;
// Классический пример: граф с разделяемыми вершинами
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<Self> {
Rc::new(Node {
value,
children: RefCell::new(vec![]),
})
}
fn add_child(&self, child: Rc<Node>) {
self.children.borrow_mut().push(child);
}
}
fn main() {
let root = Node::new(1);
let child1 = Node::new(2);
let child2 = Node::new(3);
root.add_child(Rc::clone(&child1));
root.add_child(Rc::clone(&child2));
println!("Children: {}", root.children.borrow().len()); // 2
// Паника при нарушении правил borrow:
// let _b1 = root.children.borrow();
// let _b2 = root.children.borrow_mut(); // panic!
}
try_borrow и try_borrow_mut
Чтобы не паниковать, используйте безопасные версии: try_borrow() и try_borrow_mut(), возвращающие Result.
Когда что использовать
Cell<T>— примитивные Copy-типы (u32, bool), нет накладных расходов.RefCell<T>— не-Copy типы, сложные структуры данных с несколькими владельцами черезRc<RefCell<T>>.Mutex<T>/RwLock<T>— то же самое, но потокобезопасно (Send + Sync).
Подводные камни
- Паника
RefCellв runtime трудно диагностировать в больших кодовых базах — prefer статические borrow rules везде, где возможно. CellиRefCellне являютсяSync— нельзя шарить между потоками даже черезArc.- Держать
RefMutживым через.awaitили через вызов функций, которые тоже заимствуют RefCell — верный путь к панике. - Циклические ссылки
Rc<RefCell<T>>утекают память, так как счётчики никогда не упадут до нуля. ИспользуйтеWeakдля слабых ссылок. borrow_mut()возвращает guard — если сохранить его во временную переменную с именем_, он уничтожится сразу; используйте именованную переменную.- Частое использование
RefCell— сигнал о плохой архитектуре; пересмотрите владение.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.