Что такое «неизменяемая мутабельность» и как она реализована в 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<u32>,
}
let data = MyStruct { count: Cell::new(0) };
data.count.set(42);
println!("{}", data.count.get()); // 42
- Особенность: нельзя получить обычную ссылку на внутреннее значение (&T), только скопировать его (т.е. T: Copy).
RefCell<T>
Позволяет изменять любые типы T, но проверка правил заимствования переносится с этапа компиляции на рантайм.
Пример:
<br/>use std::cell::RefCell;
struct MyStruct {
name: RefCell<String>,
}
let data = MyStruct { name: RefCell::new("Alice".to_string()) };
data.name.borrow_mut().push_str(" Smith");
println!("{}", data.name.borrow()); // "Alice Smith"
-
Если одновременно произойдёт попытка получить и borrow() (неизменяемый доступ), и borrow_mut() (мутабельный), программа паникует во время выполнения.
-
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
- OnceCell<T> и Lazy<T>
Используются для однократной инициализации значения. Это разновидность interior mutability для безопасной реализации ленивых структур.
Пример:
<br/>use std::sync::OnceLock;
static DATA: OnceLock<String> = 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 оставаться безопасным языком даже при реализации сложных паттернов работы с памятью и состоянием.