Изменяемость, то есть возможность изменить что-то, работает в Rust несколько иначе, чем в других языках. Во-первых, по умолчанию связанные имена не изменяемы:
fn main() { let x = 5; x = 6; // ошибка! }let x = 5; x = 6; // ошибка!
Изменяемость можно добавить с помощью ключевого слова mut:
let mut x = 5; x = 6; // нет проблем!
Это изменяемое связанное имя. Когда связанное имя изменяемо, это означает,
что мы можем поменять связанное с ним значение. В примере выше не то, чтобы само
значение x менялось, просто имя x связывается с другим значением типа i32.
Если же вы хотите изменить само связанное значение, вам понадобится изменяемая ссылка:
fn main() { let mut x = 5; let y = &mut x; }let mut x = 5; let y = &mut x;
y — это неизменяемое имя для изменяемой ссылки. Это значит, что y нельзя
связать ещё с чем-то (y = &mut z), но можно изменить то, на что указывает
связанная ссылка (*y = 5). Тонкая разница.
Конечно, вы можете объявить и изменяемое имя для изменяемой ссылки:
fn main() { let mut x = 5; let mut y = &mut x; }let mut x = 5; let mut y = &mut x;
Теперь y можно связать с другим значением, и само это значение тоже можно
менять.
Стоит отметить, что mut — это часть шаблона, поэтому можно делать
такие вещи:
let (mut x, y) = (5, 6); fn foo(mut x: i32) {
Однако, когда мы говорим, что что-либо «неизменяемо» в Rust, это не означает,
что оно совсем не может измениться. Мы говорим о «внешней изменяемости». Для
примера рассмотрим Arc<T>:
use std::sync::Arc; let x = Arc::new(5); let y = x.clone();
Когда мы вызываем метод clone(), Arc<T> должна обновить счётчик ссылок. Мы
не использовали модификатор mut, а значит x — неизменяемое имя. Мы не можем
получить ссылку (&mut 5) или сделать что-то подобное. И что же?
Для того чтобы понять это, мы должны вернуться назад к основам философии Rust, к сохранности памяти и механизму, гарантирующему это, к системе владения, и, в частности, к заимствованию:
Одновременно у вас может быть только один из двух перечисленных ниже видов заимствования, но не оба сразу:
- одна или более неизменяемых ссылок (
&T) на ресурс,- ровно одна изменяемая ссылка (
&mut T) на ресурс.
Итак, что же здесь на самом деле является «неизменяемым»? Безопасно ли иметь два
указателя на один объект? В случае с Arc<T>, да: изменяемый объект полностью
находится внутри самой структуры. По этой причине, метод clone() возвращает
неизменяемую ссылку (&T). Если бы он возвращал изменяемую ссылку (&mut T),
то у нас были бы проблемы. Таким образом, let mut z = Arc::new(5); объявляет
атомарный счётчик ссылок с внешней изменяемостью.
Другие типы, например те, что определены в модуле std::cell,
напротив, имеют «внутреннюю изменяемость». Например:
use std::cell::RefCell; let x = RefCell::new(42); let y = x.borrow_mut();
RefCell возвращает изменяемую ссылку &mut при помощи метода borrow_mut(). А
не опасно ли это? Что, если мы сделаем так:
use std::cell::RefCell; let x = RefCell::new(42); let y = x.borrow_mut(); let z = x.borrow_mut();
Это приведёт к панике во время исполнения. Вот что делает RefCell: он
принудительно выполняет проверку правил заимствования во время исполнения и
вызывает panic!, если они были нарушены.
Стоит отметить, что тип изменяемости — внутренняя или внешняя — определяется самим типом. Нет способа волшебно превратить значение с внутренней изменяемостью в значение со внешней, и наоборот.
Всё это подводит нас к другим аспектам правил изменяемости Rust. Давайте поговорим о них.
Изменяемость — это свойство либо ссылки (&mut), либо имени (let mut). Это
значит, что, например, у вас не может быть структуры, часть полей
которой изменяется, а другая часть — нет:
struct Point { x: i32, mut y: i32, // нельзя }
Изменяемость структуры определяется при её связывании:
fn main() { struct Point { x: i32, y: i32, } let mut a = Point { x: 5, y: 6 }; a.x = 10; let b = Point { x: 5, y: 6}; b.x = 10; // error: cannot assign to immutable field `b.x` }struct Point { x: i32, y: i32, } let mut a = Point { x: 5, y: 6 }; a.x = 10; let b = Point { x: 5, y: 6}; b.x = 10; // error: cannot assign to immutable field `b.x`
Однако, используя Cell<T>, вы можете эмулировать изменяемость на
уровне полей:
use std::cell::Cell; struct Point { x: i32, y: Cell<i32>, } let point = Point { x: 5, y: Cell::new(6) }; point.y.set(7); println!("y: {:?}", point.y);
Это выведет на экран y: Cell { value: 7 }. Мы успешно изменили значение y.