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

Как упоминалось в разделе "Хранение значений с помощью переменных", по умолчанию переменные неизменяемы. Это один из многих стимулов Rust, позволяющий писать код с использованием преимущества безопасности и удобной конкурентности (concurrency), предоставляемых 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. Будет получено сообщение об ошибке относительно неизменяемости, как показано в этом выводе:

error[E0384]: cannot assign twice to immutable variable `x`  --> src/main.rs:4:5   | 2 |     let x = 5;   |         - first assignment to `x` 3 |     println!("The value of x is: {}", x); 4 |     x = 6;   |     ^^^^^ cannot assign twice to immutable variable

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

Вы получили сообщение об ошибке 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

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

Константы

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

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

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

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

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


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

Имя константы - THREE_HOURS_IN_SECONDS, а её значение устанавливается как результат умножения 60 (количество секунд в минуте) на 60 (количество минут в часе) на 3 (количество часов, которые нужно посчитать в этой программе). Соглашение Rust для именования констант требует использования всех заглавных букв с подчёркиванием между словами. Компилятор может вычислять ограниченный набор операций во время компиляции, позволяющий записать это значение более понятным и простым для проверки способом, чем установка этой константы в значение 10 800. Дополнительную информацию о том, какие операции можно использовать при объявлении констант, см. в разделе Раздел справки 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`
  |
help: try removing the method call
  |
3 -     spaces = spaces.len();
3 +     spaces = spaces;
  |

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

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