Переменные и понятие изменяемости

Как было указано в Главе 2, по умолчанию все Rust переменные являются неизменяемыми (immutable). Это один из многих подходов Rust, который стимулирует вас писать код таким образом, чтобы использовать преимущества безопасности и лёгкого параллелизма, которые предлагает Rust. Тем не менее, у вас остаётся возможность сделать переменные изменяемыми (mutable). Давайте рассмотрим как и почему Rust поощряет неизменяемые переменные и почему вам иногда стоит отказаться от этого.

Когда переменная неизменяемая, то её значение нельзя менять, как только значение привязано к её имени. Приведём пример использования этого типа переменной. Для этого создадим новый проект variables в каталоге projects при помощи команды: cargo new variables.

Потом в созданной папке проекта variables откройте исходный файл src/main.rs и замените содержимое следующим кодом, который пока не будет компилироваться:

Файл: src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

Сохраните код программы и выполните команду cargo run. В командной строке вы увидите сообщение об ошибке:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: make this binding mutable: `mut x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables`

To learn more, run the command again with --verbose.

Данный пример показывает как компилятор помогает найти ошибки в программах. Несмотря на то, что ошибки компилятора могут вызывать разочарование, они означают, что ваша программа ещё не выполняет то, что вы от неё хотите. Ошибки не означают, что вы пока не являетесь хорошим программистом! Опытные разработчики Rust также получают ошибки компиляции.

Описание ошибки даёт понять, что причиной является то, что вы cannot assign twice to immutable variable x (не можете присвоить неизменяемой переменной новое значение), потому что вы попытались назначить второе значение неизменяемой переменной x.

Это важно, что мы получили ошибку при попытке изменить ранее не изменяемое значение во время компиляции, потому что такая ситуация может привести к дефектам в программе. Если одна часть кода работает с предположением, что данное значение никогда не изменится, а другая часть кода все-таки меняет значение, всегда существует вероятность, что первая часть кода не будет делать того, для чего она была предназначена. Причину дефектов такого рода иногда трудно отследить, особенно когда вторая часть кода меняет значение только иногда.

В Rust компилятор гарантирует, что если указано, что значение не меняется, то оно действительно не будет меняться. Это означает, что когда вы читаете или пишете код, вам не нужно отслеживать то, как и где значение может измениться. В таком случае ваш код легче понимать.

Но изменяемость может быть очень полезной. Переменные являются неизменяемыми только по умолчанию. Аналогично как вы делали в Главе 2, можно сделать переменные изменяемыми добавлением ключевого слова mut перед названием переменной. В дополнение к возможности изменить значение, указание mut передаёт намерение будущим читателям кода, что другие части кода будут изменять значение этой переменной.

Например, изменим src/main.rs на следующий код:

Файл: src/main.rs

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

Запустив программу, мы получим результат:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

Мы разрешили изменять значение, которое привязано к x со значения равного 5 на значение 6, когда стали использовать mut. В некоторых случаях, вам захочется сделать переменную изменяемой, потому что так становится проще писать код, в отличии от того, когда она осталась бы неизменяемой.

Следует учитывать множество дополнительных компромиссов для предотвращения дефектов. Например, для повышения производительности кода при работе с большими структурами данных, изменение экземпляра на месте может быть быстрее, чем копирование и возврат вновь созданного экземпляра. При небольших структурах данных создание новых экземпляров и запись кода в более функциональном стиле, может оказаться проще для понимания. В таком случае, снижение производительности может быть полезным недостатком в пользу большей ясности кода.

Различия между переменными и константами

Невозможность изменить значение переменной может напомнить другую концепцию программирования, которая есть в других языках: константы (constants). Подобно неизменяемым переменным, константы являются значениями привязанными к именам, которые не разрешено менять, но есть несколько отличий между константами и неизменяемыми переменными.

Первое, это то что с константами не разрешено использовать mut. Константы не являются не изменяемыми по умолчанию — они не изменяемые всегда.

Константы объявляются с помощью ключевого слова const вместо ключевого слова let и тип хранимых в них значений должен быть указан явно. Мы собираемся объяснить типы и аннотации типов в следующем разделе “Типы данных”, так что сейчас не волнуйтесь о деталях. Просто знайте, что для констант тип нужно всегда указывать явно.

Константы можно объявить в любой области видимости, включая глобальную. Это делает их удобными для значений, про которые должны знать многие другие части кода.

Последней разницей является то, что константы можно установить только в константное выражение, которое не является результатом вызова функции или любым другим значением, которое можно посчитать только во время выполнения.

Вот пример объявления константы с названием MAX_POINTS значение которой установлено в 100,000. Для объявления констант рекомендуется использовать все заглавные буквы с подчёркиванием между словами. Также подчёркивание можно вставлять в цифровые литералы для улучшения чтения.


#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}

Константы являются корректными для всего времени выполнения программы, внутри области видимости где они были объявлены. Это делает их удобным выбором для значений в приложении, которые могут быть доступны для многих частей приложения. Например, максимально разрешённое количество очков игрока в игре или скорость света в вакууме.

Наименование неизменяемых значений во всей программе, таких как константа, является удобным способом выразить смысл значения для будущих пользователей кода. Это помогает иметь только одно место в коде, которое придётся обновить, если будет необходимо поменять его значение в будущем.

Затенение (переменных)

Как вы видели в обучающем материале по угадыванию числа из раздела “Сравнение предположения с секретным числом” Главы 2, можно объявить переменную с тем же именем которое было использовано раньше и такая новая переменная затеняет предыдущую. Rust разработчики говорят, что первая переменная затенена (shadowed) второй переменной, что означает, что когда будут использовать такую переменную будет отображаться её второе значение, вместо первого. Можно затенять переменную, используя то же самое имя и повторяя использование ключевого слова let как в примере:

Файл: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

Программа сначала связывает значение 5 с переменной x. Затем x затеняется повторением кода let x = с помощью использования начального значения и прибавления к нему 1, так что значение x становится равным 6. Третье выражение let также затеняет переменную x, умножением предыдущего значения на 2. Это даёт переменной x финальное значение равное 12. При запуске программы мы получим вывод:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x is: 12

Затенение отличается от объявления переменной с помощью mut, так как мы получим ошибку компиляции, если случайно попробуем переназначить значение без использования ключевого слова let. Используя let, можно выполнить несколько превращений над значением, при этом оставляя переменную неизменяемой, после того как все эти превращения завершены.

Другой разницей между mut и затенением является то, что мы создаём совершенно новую переменную, когда снова используем слово let (ещё одну). Мы можем даже изменить тип значения, но снова использовать предыдущее имя. К примеру, наша программа спрашивает пользователя сколько пробелов он хочет разместить между некоторым текстом, запрашивая символы пробела, но мы на самом деле хотим сохранить данный ввод как число:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
}

Данная конструкция разрешена, потому что первая переменная spaces является строковым типом, а вторая переменная spaces числового типа. Она является совершенно новой переменной с одинаковым именем, таким же какое было у первой переменной. Таким образом, затенение избавляет нас от необходимости придумывать разные имена, вроде spaces_str и spaces_num. Вместо этого можно использовать снова более простое имя spaces. Тем не менее, если попробовать использовать для этого mut как показано ниже, то мы получим ошибку компиляции:

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
}

Ошибка говорит, что не разрешается менять тип переменной:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables`

To learn more, run the command again with --verbose.

Теперь, когда вы имеете представление о работе с переменными, посмотрим на большее количество типов данных, которые они могут иметь.