RustMiddleTechnical

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

Sources

Related topics