Что такое «неизменяемая мутабельность» и как она реализована в Rust?

Термин «неизменяемая мутабельность» (interior mutability) в Rust звучит парадоксально, потому что на первый взгляд противоречит базовым принципам языка: либо переменная мутабельна, либо неизменяема. Однако interior mutability — это специальный дизайн-паттерн, при котором внутреннее состояние объекта может изменяться, даже если сам объект объявлен как неизменяемый (&T).

Это поведение реализуется в обход стандартной системы заимствований Rust с помощью особых типов из стандартной библиотеки, которые используют unsafe внутри, но предоставляют безопасный интерфейс снаружи.

Зачем вообще нужна “неизменяемая мутабельность”?

Rust строго разграничивает владение, заимствование и мутабельность:

  • &T — неизменяемая ссылка.

  • &mut T — изменяемая ссылка.

В обычной ситуации, если у вас есть &T, вы не можете изменять объект. Это защищает от гонок данных и других проблем в многопоточном контексте. Однако бывают случаи, когда необходимо изменить внутреннее состояние, не нарушая интерфейс (например, ленивая инициализация, счётчики ссылок, кэширование и др.).

Именно тогда и используется interior mutability.

Основные типы, реализующие “неизменяемую мутабельность”

Cell<T>
Позволяет изменять Copy-типы без получения &mut.

Пример:

<br/>use std::cell::Cell;
struct MyStruct {
count: Cell&lt;u32&gt;,
}
let data = MyStruct { count: Cell::new(0) };
data.count.set(42);
println!("{}", data.count.get()); // 42
  1. Особенность: нельзя получить обычную ссылку на внутреннее значение (&T), только скопировать его (т.е. T: Copy).

RefCell<T>
Позволяет изменять любые типы T, но проверка правил заимствования переносится с этапа компиляции на рантайм.

Пример:

<br/>use std::cell::RefCell;
struct MyStruct {
name: RefCell&lt;String&gt;,
}
let data = MyStruct { name: RefCell::new("Alice".to_string()) };
data.name.borrow_mut().push_str(" Smith");
println!("{}", data.name.borrow()); // "Alice Smith"
  1. Если одновременно произойдёт попытка получить и borrow() (неизменяемый доступ), и borrow_mut() (мутабельный), программа паникует во время выполнения.

  2. Mutex<T> и RwLock<T>
    Эти типы из модуля std::sync предоставляют interior mutability в многопоточной среде. Они синхронизируют доступ к данным между потоками.

    • Mutex<T> — обеспечивает эксклюзивный доступ.

    • RwLock<T> — позволяет множественный читательский доступ или один писательский.

Пример:

<br/>use std::sync::Mutex;
let counter = Mutex::new(0);
{
let mut num = counter.lock().unwrap();
\*num += 1;
}
println!("{:?}", counter.lock().unwrap()); // 1
  1. OnceCell<T> и Lazy<T>
    Используются для однократной инициализации значения. Это разновидность interior mutability для безопасной реализации ленивых структур.

Пример:

<br/>use std::sync::OnceLock;
static DATA: OnceLock&lt;String&gt; = OnceLock::new();
fn get_data() -> &'static str {
DATA.get_or_init(|| "Hello".to_string())
.as_str()
}

Как это реализовано внутри?

Все эти типы используют низкоуровневую магию:

  • Cell<T> и RefCell<T> применяют unsafe и std::ptr для обхода ограничений.

  • RefCell<T> ведёт счётчики заимствований в рантайме: сколько borrow() и есть ли активный borrow_mut().

  • Mutex и RwLock используют системные примитивы блокировки (pthread_mutex_t, futex и аналоги).

  • Все они оборачивают внутреннее значение в UnsafeCell<T> — единственный тип, который может быть мутабельно доступен через &T, что формально нарушает правило, но разрешено для реализации таких паттернов.

Когда стоит использовать “interior mutability”?

  • Когда API требует &self, но вы хотите модифицировать внутреннее состояние (например, логгеры, кэши).

  • При использовании переиспользуемых структур, доступных в разных местах, но без необходимости брать &mut self.

  • В реализации ленивой инициализации.

  • В многопоточной среде, где нужен контролируемый доступ к изменяемым данным.

Пример из реальной жизни: Rc<RefCell

Это очень популярная комбинация:

use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(vec!\[1, 2, 3\]));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push(4);
b.borrow_mut().push(5);
println!("{:?}", shared.borrow()); // \[1, 2, 3, 4, 5\]

Объект живёт в куче (Rc), может быть разделён между владельцами, и его внутреннее состояние можно менять (RefCell), даже имея только &Rc.

Таким образом, “неизменяемая мутабельность” — это продуманный компромисс между безопасностью и гибкостью. Она позволяет Rust оставаться безопасным языком даже при реализации сложных паттернов работы с памятью и состоянием.