# Внутренняя мутабельность в Rust: паттерн использования Cell, RefCell

> Разбираемся с паттерном внутренней мутабельности (Interior Mutability) в Rust: философия работы, контейнеры Cell и RefCell, отличие от унаследованной мутабельности.

- **URL:** https://eagle2000.space/rust/rust-interior-mutability/
- **Date:** 2026-09-09

- **Tags:** rust, dev, interior-mutability, cell, refcell, mutex

---
## Введение

Обычно borrow checker действует жестко: объявляем переменную без `mut` — и компилятор ни за что не даст изменять её поля по иммутабельной ссылке `&T`. Это стандартная **унаследованная мутабельность (Inherited Mutability)**.

Но на практике регулярно возникают ситуации, когда компоненту нужно сохранить закэшированный ответ, записать ошибку в журнал или обновить внутренний идентификатор, имея на руках только неизменяемую ссылку `&self`.

Для таких случаев в стандартной библиотеке Rust есть паттерн **Внутренней мутабельности (Interior Mutability)**. В этой статье разберем, как устроены `Cell` и `RefCell`, где они применяются на практике и чем отличаются от других контейнеров.

---

## 1. Контейнер Cell<T>: Мутабельность без ссылок

Самый простой инструмент внутренней мутабельности — тип **`std::cell::Cell<T>`**.

Особенность `Cell<T>` в том, что он **никогда не выдает прямых ссылок** `&T` или `&mut T` на то значение, которое лежит внутри него.

Вместо ссылок `Cell` работает путем цельного чтения, записи или перемещения данных:
* `get()` — возвращает копию внутреннего значения (требует, чтобы `T: Copy`).
* `set(val)` — полностью заменяет внутреннее значение на `val`.
* `replace(val)` — помещает `val` внутрь контейнера и возвращает ранее хранившееся там значение.
* `take()` — извлекает значение, оставляя на его месте `Default::default()`.

```rust
use std::cell::Cell;

struct Counter {
    count: Cell<u32>,
}

fn main() {
    let c = Counter { count: Cell::new(0) };
    
    // Переменная 'c' НЕ мутабельна! Но мы спокойно меняем count:
    c.count.set(c.count.get() + 1);
    
    println!("Текущий счетчик: {}", c.count.get()); // Выведет 1
}
```

> [!NOTE]
> Запомните: если тип `T` не реализует типаж `Copy` (например, `String` или `Vec`), метод `.get()` не скомпилируется. Однако со сложными типами без `Copy` в `Cell` по-прежнему можно работать через `.set()`, `.replace()` и `.take()`.

> [!TIP]
> Поскольку `Cell<T>` не выдает ссылок на внутреннее содержимое, ему **не нужно отслеживать счётчик заимствований**. В нем полностью отсутствуют накладные расходы рантайма (Zero-cost abstraction) и риск паники! Это идеальный выбор для флагов, счетчиков и `Copy`-типов.

---

## 2. Контейнер RefCell<T>: Динамический Borrow Checker

Если внутри контейнера лежит сложная структура (например, `Vec<String>`, `HashMap` или пользовательский объект), скопировать ее полностью через `Cell::get()` не получится. 

Для сложных типов используется **`std::cell::RefCell<T>`**.

`RefCell<T>` позволяет запрашивать ссылки на внутреннее содержимое:
* `.borrow()` — возвращает обертку `Ref<T>`, работающую как обычная иммутабельная ссылка `&T`.
* `.borrow_mut()` — возвращает обертку `RefMut<T>`, работающую как мутабельная ссылка `&mut T`.
* `.try_borrow()` / `.try_borrow_mut()` — безопасные варианты, возвращающие `Result`. Они позволяют проверить заимствование и избежать вызова паники при конфликте ссылок.

### Проверки заимствования в рантайме

Обычный компилятор Rust проверяет правило: *«или много `&`, или ровно один `&mut`»* во время компиляции. 

`RefCell` переносит этот счетчик заимствований на этап **выполнения программы (runtime)**. 

```rust
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    let mut b1 = data.borrow_mut();
    b1.push(4);

    // ВНИМАНИЕ: b1 еще активен! Попытка сделать второй borrow_mut вызовет ПАНИКУ:
    // let mut b2 = data.borrow_mut(); // ALREADY BORROWED PANIC!
}
```

Благодаря идиоме RAII, смарт-указатели `Ref` и `RefMut` в своём деструкторе `Drop` автоматически уменьшают счётчик заимствований `RefCell` при выходе из области видимости. Поэтому если вы явно вызовите `drop(b1)`, следующий вызов `data.borrow_mut()` отработает без ошибок.

> [!CAUTION]
> Помните: `RefCell` не отменяет правила заимствования Rust, он лишь откладывает их проверку до рантайма. Ошибки проектирования приведут не к ошибке компиляции, а к падению программы!

---

## 3. Где это пригодится в реальном коде

Теперь, когда мы знаем, как работают `Cell` и `RefCell`, разберем, зачем менять данные через неизменяемую ссылку `&self`, если можно просто прописать `&mut self`.

В реальном коде сделать `&mut self` получается не всегда. Вот три частых сценария:

### 1. Счётчик кликов у кнопки
Возьмём обычный UI-компонент кнопки. Попробуем обновить счётчик кликов в методе `&self`:


### 💻 Сравнение: Ошибка компиляции vs Решение с Cell


#### Шаг 1

```rust
// ?compile_fail

struct Button {
    label: String,
    click_count: u32,
}

impl Button {
    fn on_click(&self) {
        println!("Кнопка '{}' нажата!", self.label);
        self.click_count += 1;
    }
}

fn main() {
    let btn = Button {
        label: String::from("Отправить"),
        click_count: 0,
    };
    btn.on_click();
}
```

**Без Cell: Ошибка компиляции**
- Компилятор запрещает изменять поле `click_count` по неизменяемой ссылке `&self`.
- Сделать метод `&mut self` в UI-компонентах часто нельзя, так как кнопка доступна одновременно из разных частей приложения.


#### Шаг 2

```rust
use std::cell::Cell;

struct Button {
    label: String,
    click_count: Cell<u32>,
}

impl Button {
    fn on_click(&self) {
        println!("Кнопка '{}' нажата!", self.label);
        let count = self.click_count.get();
        self.click_count.set(count + 1);
    }
}

fn main() {
    let btn = Button {
        label: String::from("Отправить"),
        click_count: Cell::new(0),
    };

    btn.on_click();
    btn.on_click();
    println!("Всего кликов по кнопке: {}", btn.click_count.get());
}
```

**Решение с Cell: Код компилируется**
- Орачиваем счетчик в `Cell<u32>`.
- Метод `on_click(&self)` успешно обновляет значение через `.set()`, сохраняя ссылку `&self`.


В UI-библиотеках методы обработки событий работают через ссылки `&self`, так как кнопка вызывается из разных частей приложения. `Cell` дает возможность менять внутренний счётчик кликов прямо по `&self` без пробрасывания `&mut self` по всей цепочке вызовов.

### 2. Накопление истории логов в сервисе
Ещё один кейс — общая структура `App`, к которой компоненты обращаются по ссылке `&App` для чтения настроек. С `RefCell` любая функция может на лету записать предупреждение в общий массив логов:

```rust
struct App {
    name: String,
    // Список логов прямо внутри приложения
    logs: RefCell<Vec<String>>,
}

impl App {
    fn log_error(&self, msg: &str) {
        // Добавляем запись в список, имея только &self
        self.logs.borrow_mut().push(msg.to_string());
    }
}
```

### 3. Флаг состояния «уже выполнено»
То же самое с флагами инициализации в фоновом плеере или загрузчике:

```rust
struct Player {
    is_playing: Cell<bool>,
}

impl Player {
    fn play(&self) {
        if !self.is_playing.get() {
            println!("Включаем воспроизведение...");
            self.is_playing.set(true);
        }
    }
}
```

---

## 4. Ограничение по многопоточности

Важный нюанс: контейнеры `Cell` и `RefCell` предназначены **строго для однопоточного кода**.

Оба типа не реализуют маркерный типаж `Sync`, поэтому передать ссылку на них в другой поток не выйдет — компилятор заблокирует такую попытку еще при сборке. 

Подробный разбор многопоточной внутренней мутабельности (`Mutex`, `RwLock`), а также типажей `Send` и `Sync` выйдет в отдельной статье.

---

| Тип | Выдает ссылки? | Проверка заимствования | Требования к типам / Трейтам | Накладные расходы |
| :--- | :---: | :---: | :---: | :--- |
| **`Cell<T>`** | Нет (только get/set) | Не требуется | Требует `T: Copy` для `.get()` | Нулевые (Zero-cost) |
| **`RefCell<T>`** | Да (`Ref` / `RefMut`) | Runtime (Паника при нарушениях) | Работает с любым `T` | Небольшой счетчик в рантайме |

Давайте изучим работу этих типов на практических слайдах:


### 💻 Сравнение работы Cell<T> и RefCell<T>


#### Шаг 1

```rust
use std::cell::Cell;

struct Logger {
    log_count: Cell<usize>,
}

impl Logger {
    fn new() -> Self {
        Logger {
            log_count: Cell::new(0),
        }
    }

    // Обратите внимание: метод принимает &self (неизменяемую ссылку!), но меняет log_count!
    fn log(&self, message: &str) {
        println!("[LOG]: {message}");
        let current = self.log_count.get();
        self.log_count.set(current + 1);
    }
}

fn main() {
    let logger = Logger::new();

    logger.log("Первое сообщение системы");
    logger.log("Второе сообщение системы");

    println!("Всего записей в логе: {}", logger.log_count.get());

    // Метод replace заменяет значение и возвращает старое
    let old_val = logger.log_count.replace(100);
    println!("Старый счетчик: {old_val}, новый счетчик: {}", logger.log_count.get());
}
```

**Внутренняя мутабельность с Cell<T>**
- `Cell<T>` позволяет изменять значение внутри объекта, имея только неизменяемую ссылку `&self`.
- Запись `set()` и чтение `get()` работают путем копирования или перемещения значения без создания ссылок `&T`.
- Идеально подходит для простых типов (`Copy`) или флагов и счетчиков.


#### Шаг 2

```rust
use std::cell::RefCell;

struct UserCache {
    cache: RefCell<Vec<String>>,
}

impl UserCache {
    fn new() -> Self {
        UserCache {
            cache: RefCell::new(Vec::new()),
        }
    }

    fn add_user(&self, name: String) {
        // Запрашиваем изменяемое заимствование во время выполнения
        self.cache.borrow_mut().push(name);
    }

    fn print_users(&self) {
        // Запрашиваем неизменяемое заимствование
        let users = self.cache.borrow();
        println!("Пользователи в кэше: {:?}", *users);
    }
}

fn main() {
    let cache = UserCache::new();

    cache.add_user("Алиса".to_string());
    cache.add_user("Борис".to_string());

    cache.print_users();

    // Проверка размера кэша
    println!("Количество пользователей: {}", cache.cache.borrow().len());
}
```

**Динамическое заимствование с RefCell<T>**
- `RefCell<T>` позволяет изменять сложные структуры в куче через `borrow_mut()` по ссылкам `&self`.
- В отличие от `Cell`, `RefCell` выдает временные ссылки `Ref` и `RefMut`.
- Проверки заимствований переносятся на runtime. Попытка открыть два `borrow_mut()` одновременно вызовет панику.


---

## Проверь свои знания!

Пройдите короткий интерактивный тест по теме внутренней мутабельности:


### 📝 Внутренняя мутабельность в Rust


**1. В чём заключается суть паттерна внутренней мутабельности (Interior Mutability)?**

- [ ] Автоматическое копирование всех переменных в кучу
- [ ] Изменение внутренних данных структуры через неизменяемую ссылку &self
- [ ] Полный отказ от проверки типов во время компиляции
- [ ] Увеличение размера стека для сложных объектов

**2. Какой из методов `Cell<T>` забирает внутреннее значение, оставляя на его месте `T::default()`?**

- [ ] Cell::take()
- [ ] Cell::get()
- [ ] Cell::replace()
- [ ] Cell::drop()

**3. В чём ключевое отличие `RefCell<T>` от `Cell<T>`?**

- [ ] Cell компилируется в unsafe код без проверок
- [ ] RefCell выдаёт временные умные ссылки Ref/RefMut на внутренний объект любой сложности, а Cell копирует или перемещает значение целиком
- [ ] RefCell не работает со структурами данных
- [ ] Cell можно использовать только с вектором строк

**4. Что произойдёт во время выполнения, если вызвать `.borrow_mut()` на `RefCell`, когда первое изменяемое заимствование ещё активно?**

- [ ] Компилятор выдаст ошибку при сборке
- [ ] Поток заблокируется до освобождения ссылки
- [ ] Программа запаникует во время работы (Already borrowed panic)
- [ ] Данные автоматически продублируются в памяти

**5. Какое требование предъявляется к типу `T` для вызова метода `.get()` у `Cell<T>`?**

- [ ] Тип T должен реализовывать типаж Copy
- [ ] Тип T должен быть обёрнут в Option
- [ ] Тип T должен реализовывать типаж Display
- [ ] Тип T должен храниться только в куче

**6. В какой ситуации `Cell<u32>` предпочтительнее `RefCell<u32>`?**

- [ ] Когда нужно заимствовать элементы по длинным ссылкам
- [ ] Когда требуется счетчик или флаг с нулевым оверхедом и гарантированным отсутствием паник в рантайме
- [ ] Когда данные нужно передавать между потоками
- [ ] Никогда, RefCell всегда эффективнее

**7. Что возвращает метод `.borrow_mut()` у контейнера `RefCell<T>`?**

- [ ] Обычную сырую ссылку &mut T
- [ ] Умную обёртку RefMut<T>, которая уменьшает счётчик заимствований при своём уничтожении (Drop)
- [ ] Указатель *mut T
- [ ] Новый экземпляр RefCell<T>

**8. Как безопасно попытаться получить заимствование в `RefCell<T>` без риска вызвать панику программы?**

- [ ] Использовать методы try_borrow() и try_borrow_mut(), возвращающие Result
- [ ] Обернуть код в блок unsafe
- [ ] Вызвать метод std::mem::forget
- [ ] Использовать макрос panic_catch!

**9. Какой метод `Cell<T>` помещает новое значение внутрь контейнера и возвращает ранее хранившееся значение?**

- [ ] Cell::set()
- [ ] Cell::get()
- [ ] Cell::replace()
- [ ] Cell::take()

**10. Какая ошибка произойдет, если запросить `.borrow_mut()`, пока держится активная неизменяемая ссылка через `.borrow()` у `RefCell`?**

- [ ] Компилятор заблокирует сборку
- [ ] Паника во время работы: нарушено правило о невозможности совмещения & и &mut
- [ ] Программа молча проигнорирует запрос
- [ ] Старая ссылка автоматически аннулируется

**11. Какова величина накладных расходов у `Cell<T>` по сравнению с обычным полем структуры?**

- [ ] Значительный оверхед из-за мьютексов
- [ ] Нулевая (Zero-cost) — в скомпилированном коде Cell представляет собой простое значение в памяти
- [ ] Постоянный выделение памяти в куче
- [ ] Замедление на каждый вызов на 50%

**12. Что происходит со счётчиком заимствований `RefCell`, когда объект `Ref` или `RefMut` выходит из области видимости?**

- [ ] Счётчик обнуляется мгновенно в 0
- [ ] Автоматически вызывается drop(), который уменьшает соответствующий счётчик заимствований
- [ ] Счётчик никогда не уменьшается до перезапуска программы
- [ ] Счётчик увеличивается на 1

**13. Можно ли использовать `Cell<String>` со сложным типом `String`, у которого нет типажа `Copy`?**

- [ ] Да, но метод .get() будет недоступен; работать можно через set(), replace() и take()
- [ ] Нет, Cell работает строго с типами f32 и u32
- [ ] Да, но только в unsafe блоках
- [ ] Нет, компилятор запрещает создавать Cell<String>

**14. Почему последовательный вызов b1 = data.borrow_mut(); drop(b1); b2 = data.borrow_mut(); НЕ вызовет панику?**

- [ ] Первое заимствование b1 завершилось и было освобождено до вызова b2
- [ ] RefCell разрешает сколько угодно мутабельных ссылок
- [ ] drop() отключает проверку заимствований
- [ ] Компилятор объединяет два заимствования в одно

**15. Для какого сценария `RefCell<Vec<String>>` подходит идеально?**

- [ ] Для многопоточного сервера с 100 потоками
- [ ] Для вычисления числовых констант в comptime
- [ ] Для структуры сервиса, передаваемой по &self, которой нужно накапливать логи в вектор
- [ ] Для создания глобальных неизменяемых констант

**16. Что будет, если вызвать `Cell::set(new_val)` у `Cell<u32>`?**

- [ ] Старое значение будет перезаписано новым `new_val`
- [ ] Метод вернёт ошибку Result
- [ ] Значение добавится в конец очереди
- [ ] Программа заблокирует вызовы к структуре

**17. Какое базовое правило заимствований Rust переносит `RefCell` на runtime?**

- [ ] Либо одна мутабельная ссылка &mut T, либо произвольное количество неизменяемых ссылок &T
- [ ] Переменные должны быть заглавными буквами
- [ ] Все структуры должны быть Copy
- [ ] Переменные из стека переносятся в кучу

