Переменные и изменяемость
Как упоминалось в разделе "Хранение значений с помощью переменных" , по умолчанию, переменные неизменяемы. Это — одно из средств Rust, побуждающее вас писать код так, чтобы он использовал преимущества безопасности и лёгкого параллелизма, предоставляемые языком. Однако, вы всё же можете делать свои переменные изменяемыми. Давайте рассмотрим, как и почему Rust побуждает вас отдавать предпочтение неизменяемости данных, и почему вам иногда может захотеться отказаться от неё.
Если переменная неизменяема, то однажды присвоив ей значение, вы не сможете его поменять. Чтобы проиллюстрировать это, создадим в директории projects новый проект variables. Выполним для этого команду cargo new variables
.
Затем, в вашей новой директории variables, откройте src/main.rs и замените его код тем, что ниже (пока что он не будет компилироваться):
Файл: src/main.rs
fn main() {
let x = 5;
println!("Значение x: {x}");
x = 6;
println!("Значение x: {x}");
}
Save and run the program using cargo run
. You should receive an error message regarding an immutability error, as shown in this output:
$ 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`
3 | println!("Значение x: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Этот пример демонстрирует, как компилятор помогает вам находить ошибки в ваших программах. Ошибки компилятора могут быть вводящими в ступор, но на деле они всего лишь значат, что ваша программа работает не так, как вы от неё, скорее всего, ожидаете. Они не означают, что вы — плохой программист! Опытные программисты на Rust регулярно сталкиваются с ошибками компилятора.
Вы получили сообщение об ошибке (cannot assign twice to immutable variable `x`
), поскольку пытаетесь связать второе значение с неизменяемой переменной x
.
Это важно, что мы получаем ошибки, связанные с неизменямостью, в процессе компиляции, поскольку они часто становятся причиной разных багов. Если одна часть нашего кода работает в предположении о том, данное значение никогда не изменится, но другая часть кода это значение меняет, то первая часть наверняка будет работать не так, как задумано. Причину подобных багов, когда они проявляют себя, бывает очень тяжело отыскать — особенно, если вторая часть кода вторгается в переменную лишь иногда. Компилятор Rust гарантирует, что если вы указали, что переменная не изменится, она в самом деле никогда не изменится, так что вам не нужно постоянно держать в голове память о том, что может изменяться, а что — нет. Ваш код становится банально проще понимать.
Однако изменяемость тоже крайне необходима; она тоже нужна, чтобы писать простой и ясный код. Хотя переменные по умолчанию неизменяемы, вы можете сделать их изменяемыми, добавив перед названием переменной mut
— прямо как вы делали в Главе 2. Добавление mut
также служит как маркер будущим читателям вашего кода: он показывает, что другиv частям кода можно изменять значение этой переменной.
Например, давайте поменяем src/main.rs вот так:
Файл: src/main.rs
fn main() { let mut x = 5; println!("Значение x: {x}"); x = 6; println!("Значение x: {x}"); }
Когда мы теперь запустим программу, мы увидим:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
Значение x: 5
Значение x: 6
Мы смогли поменять значение, связанное с x
, с 5
на 6
, поскольку использовали mut
. В конечном счёте, вопрос об использовании изменяемости остаётся за вами и определяется тем, какое решение, по вашему мнению, будет чище.
Константы
Как и неизменяемые переменные, константы — это привязанные к имени неизменяемые значения. Однако, между константами и неизменяемыми переменными разница есть.
Во-первых, вы не можете использовать mut
с константами. Константы не просто неизменяемы по умолчанию — они вообще неизменяемы, всегда. Константы объявляются с помощью ключевого слова const
(вместо ключевого слова let
), а также они должны быть аннотированы типом. Мы расскажем подробнее о типах и аннотациях типа в следующем разделе — "Типы данных"; так что пока что не задумывайтесь много о деталях. Просто помните, что вы обязаны аннотировать тип констант.
Константы могут быть объявлены в любой области видимости, в том числе и в глобальной, что делает их полезными для объявления значений, которые используются в программе повсеместно.
Последнее отличие констант от неизменяемых переменных состоит в том, что константы могут связываться только с константыми выражениями — то есть такими, которые могут быть вычислены на этапе компиляции.
Вот пример объявление константы:
#![allow(unused)] fn main() { const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; }
The constant’s name is THREE_HOURS_IN_SECONDS
and its value is set to the result of multiplying 60 (the number of seconds in a minute) by 60 (the number of minutes in an hour) by 3 (the number of hours we want to count in this program). Rust’s naming convention for constants is to use all uppercase with underscores between words. The compiler is able to evaluate a limited set of operations at compile time, which lets us choose to write out this value in a way that’s easier to understand and verify, rather than setting this constant to the value 10,800. See the Rust Reference’s section on constant evaluation for more information on what operations can be used when declaring constants.
Константы существуют в памяти в течение всего времени исполнения программы — в той области видимости, в которой были объявлены. Это свойство делает константы полезными для определения значений в программе, многим частям которой необходимо знать, например, скорость света или о максимальном количестве очков, которые может получить игрок.
Использование констант для именования жёстко закодированных значений, используемых в вашей программе тут и там, также полезно тем, что все, кто будут работать над вашим кодом, будут иметь представление о смысле встречающихся значений. Эта же практика позволяет выделять в одно место такие значения, которые жёстко кодируются, но потенциально могут измениться в будущем.
Затенение
Как вы увидели в Главе 2 ,вы можете объявить новую переменную с тем же именем, которое носит уже существующая переменная. Считается, что старая переменная затеняется новой, что означает, что при использовании данного имени компилятор будет искать значение именно в новой переменной. Старая переменная не будет видна до тех пор, пока не закончится область видимости новой. Кроме того, затеняющая переменная сама может быть затенённой. Мы можем затенить переменную, использовав её же имя и повторно использовав его с ключевым словом let
; вот так:
Файл: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("Значение x во внутренней области видимости: {x}"); } println!("Значение x: {x}"); }
Сначала, эта программа связывает x
со значением 5
. Затем она создаёт новую переменную x
, повторно используя конструкцию let x =
, присваивая нового x
значение старого x
и прибавляя к нему 1
; новый x
таким образом имеет значение 6
. Затем, во внутренней области видимости, создаваемой фигурными скобками, третью инструкцию let
также затеняет x
и создаёт новую переменную, умножая предыдущее значение на 2
и приписывая тем самым переменной x
значение 12
. Когда область видимости заканчивается, внутреннее затенение также заканчивается, и x
обратно становится равным 6
. Если мы запустим программу, её вывод будет таков:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
Значение x во внутренней области видимости: 12
Значение x: 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` (bin "variables") due to 1 previous error
Теперь мы знаем, как работают переменные. Давайте теперь подробнее посмотрим на типы данных, которые эти переменные могут хранить.