Изменяемость (mutability)

Изменяемость, то есть возможность изменить что-то, работает в Rust несколько иначе, чем в других языках. Во-первых, по умолчанию связанные имена не изменяемы:

let x = 5;
x = 6; // ошибка!

Изменяемость можно добавить с помощью ключевого слова mut:

let mut x = 5;

x = 6; // нет проблем!

Это изменяемое связанное имя. Когда связанное имя изменяемо, это означает, что мы можем поменять связанное с ним значение. В примере выше не то, чтобы само значение x менялось, просто имя x связывается с другим значением типа i32.

Если же вы хотите изменить само связанное значение, вам понадобится изменяемая ссылка:

let mut x = 5;
let y = &mut x;

y — это неизменяемое имя для изменяемой ссылки. Это значит, что y нельзя связать ещё с чем-то (y = &mut z), но можно изменить то, на что указывает связанная ссылка (*y = 5). Тонкая разница.

Конечно, вы можете объявить и изменяемое имя для изменяемой ссылки:

let mut x = 5;
let mut y = &mut x;

Теперь y можно связать с другим значением, и само это значение тоже можно менять.

Стоит отметить, что mut — это часть шаблона, поэтому можно делать такие вещи:

let (mut x, y) = (5, 6);

fn foo(mut x: i32) {
# }

Внутренняя (interior) и внешняя (exterior) изменяемость

Однако, когда мы говорим, что что-либо «неизменяемо» в Rust, это не означает, что оно совсем не может измениться. Мы говорим о «внешней изменяемости». Для примера рассмотрим [Arc<T>]arc:

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]stdcell, напротив, имеют «внутреннюю изменяемость». Например:

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();
# (y, z);

Это приведёт к панике во время исполнения. Вот что делает RefCell: он принудительно выполняет проверку правил заимствования во время исполнения и вызывает panic!, если они были нарушены.

Стоит отметить, что тип изменяемости — внутренняя или внешняя — определяется самим типом. Нет способа волшебно превратить значение с внутренней изменяемостью в значение со внешней, и наоборот.

Всё это подводит нас к другим аспектам правил изменяемости Rust. Давайте поговорим о них.

Изменяемость на уровне полей

Изменяемость — это свойство либо ссылки (&mut), либо имени (let mut). Это значит, что, например, у вас не может быть структуры, часть полей которой изменяется, а другая часть — нет:

struct Point {
    x: i32,
    mut y: i32, // нельзя
}

Изменяемость структуры определяется при её связывании:

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>]cell, вы можете эмулировать изменяемость на уровне полей:

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.