Изменяемость, то есть возможность изменить что-то, работает в 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
.