Что такое мутабельность (mutability) в Rust и как она работает?
В Rust мутабельность (mutability) — это система, контролирующая возможность изменять (модифицировать) значения переменных. Она является неотъемлемой частью модели владения и безопасности языка, помогая компилятору на этапе компиляции предотвратить гонки данных, неопределённое поведение и ошибки в многопоточной среде.
Rust делает мутабельность явной: по умолчанию все переменные неизменяемы (immutable), и только при использовании ключевого слова mut переменной разрешается изменять своё значение. Это работает как с простыми переменными, так и с заимствованиями и структурами.
Основы мутабельности
Чтобы изменить значение переменной, её нужно объявить с mut:
fn main() {
let mut x = 5;
x = x + 1;
println!("{}", x); // 6
}
Если бы x была объявлена без mut, то попытка присвоить новое значение вызвала бы ошибку компиляции.
Почему в Rust всё по умолчанию неизменяемо?
Это философия Rust: чем меньше свободы, тем больше безопасности. Неизменяемые переменные:
-
Гарантируют предсказуемость кода.
-
Исключают ошибки, связанные с неожиданной модификацией.
-
Облегчают оптимизацию компилятором.
-
Улучшают читаемость и отладку.
Мутабельность и ссылки
Мутабельность касается не только переменных, но и ссылок. В Rust запрещается одновременно иметь:
-
более одной мутабельной ссылки (&mut T),
-
или одновременно мутабельные и немутабельные ссылки (&T и &mut T) на один и тот же объект.
Это правило известно как правило единственного мутабельного доступа и предотвращает гонки данных на этапе компиляции.
Пример корректного использования мутабельной ссылки:
fn increment(x: &mut i32) {
\*x += 1;
}
fn main() {
let mut num = 10;
increment(&mut num);
println!("{}", num); // 11
}
Здесь переменная num объявлена с mut, и мы передаём в функцию мутабельную ссылку. Это позволяет функции изменить значение переменной.
Ошибка при одновременном заимствовании
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // ошибка компиляции!
println!("{}, {}", r1, r2);
}
В этом примере r1 и r2 — неизменяемые ссылки. Rust не разрешает в это же время создавать мутабельную ссылку r3. Это нарушает правило исключительности и приведёт к ошибке компиляции. Причина — Rust не может гарантировать отсутствие конфликтов между одновременными доступами.
Мутабельность структур
Если вы хотите изменить поля структуры, то не только переменная должна быть mut, но и доступ к полю должен идти через мутабельную ссылку:
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut p = Point { x: 1, y: 2 };
p.x = 5; // OK
}
Если p не объявлен как mut, попытка изменить p.x приведёт к ошибке.
Мутабельность и владение
Когда вы передаёте переменную в функцию по значению, то передаёте владение. Если она не копируемая (не реализует Copy), вы больше не сможете её использовать после передачи.
Однако если вы хотите, чтобы функция изменила значение, то передайте мутабельную ссылку (&mut), и функция сможет работать с оригиналом:
fn update(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
update(&mut s);
println!("{}", s); // hello world
}
Разница между мутабельной переменной и мутабельной ссылкой
Иногда это вызывает путаницу, но mut перед переменной — это одно, а &mut — другое.
let mut x = 5; // x можно изменять
let y = &mut x; // y — мутабельная ссылка на x
\*y += 1;
Также возможен случай, когда переменная мутабельная, но ссылка на неё — нет:
let mut s = String::from("hello");
let r = &s; // r — неизменяемая ссылка, даже если s — mut
И наоборот:
let s = String::from("hello");
let mut r = &s; // сама ссылка может быть изменена, но не содержимое
Вложенная мутабельность (interior mutability)
Иногда требуется изменить данные даже если они доступны через неизменяемую ссылку. В обычном Rust-коде это невозможно, но существует специальный механизм — вложенная мутабельность, реализуемый через типы вроде RefCell<T> и Cell<T> из std::cell.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
\*data.borrow_mut() += 1;
println!("{}", data.borrow());
}
В этом примере data не является mut, но мы всё равно можем изменить его содержимое. Это возможно, потому что RefCell проверяет правила заимствования во время выполнения, а не на этапе компиляции.
Мутабельность и многопоточность
В многопоточном коде требуется особая осторожность с мутабельностью. Rust предлагает безопасные обёртки, такие как:
-
Mutex<T> — позволяет безопасно изменять данные из нескольких потоков, обеспечивая эксклюзивный доступ;
-
RwLock<T> — разрешает несколько одновременных чтений, но только одно запись;
-
Atomic<T> — примитивы для атомарных операций без блокировок.
Все они сочетают концепции мутабельности, владения и безопасной конкурентности.