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

Как упоминалось в разделе “Сохранение значений в переменных”, по умолчанию переменные являются неизменяемыми. Это одна из многих подсказок, которые Rust даёт вам для написания кода таким образом, чтобы использовать преимущества безопасности и простого параллелизма, которые предлагает Rust. Однако у вас есть возможность сделать ваши переменные изменяемыми. Давайте рассмотрим, как и почему 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: consider making this binding mutable: `mut x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

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

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

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

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

Но изменяемость может быть очень полезной и может сделать код более удобным для написания. Хотя переменные неизменяемы по умолчанию, вы можете сделать их изменяемыми, добавив mut перед именем переменной, как вы делали это в Главе 2. Добавление 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

Когда используется mut, мы можем изменять значение, привязанное к x, с 5 до 6. В конечном счёте, решение о том, использовать изменяемость или нет, зависит от вас и от того, что вы считаете наиболее приемлемым в данной конкретной ситуации.

Константы

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

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

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

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

Вот пример объявления константы:


#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

Имя константы THREE_HOURS_IN_SECONDS и её значение устанавливается в результате умножения числа 60 (количество секунд в минуте) на 60 (количество минут в часе) на 3 (количество часов, которое мы хотим подсчитать в этой программе). Соглашение об именах констант в Rust состоит в том, чтобы использовать все символы в верхнем регистре с символами подчёркивания между словами. Компилятор способен вычислить ограниченный набор операций во время компиляции, что позволяет нам записать это значение так, чтобы его было легче понять и проверить, вместо того, чтобы устанавливать для этой константы значение 10800. См раздел справочника Rust, посвящённый вычислению констант для получения дополнительной информации о том, какие операции можно использовать при объявлении констант.

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

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

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

Как вы видели в руководстве по игре Угадайка в Главе 2 , вы можете объявить новую переменную с тем же именем, что и предыдущая переменная. Rustaceans говорят, что первая переменная затенена второй, а это значит, что компилятор увидит вторую переменную, когда вы воспользуетесь именем переменной. По сути, вторая переменная затеняет первую, присваивая себе любое использование имени переменной до тех пор, пока либо она сама не будет затенена, либо область действия не закончится. Мы можем затенить переменную, используя то же имя переменной и повторив использование ключевого слова let следующим образом:

Файл: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

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

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

$ 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 in the inner scope is: 12
The value of x is: 6

Затенение отличается от объявления переменной с помощью 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
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

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

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