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

Как Вы знаете, по умолчанию все Rust-переменные неизменяемые. Это одна из особенностей (рекомендаций) языка Rust, которая позволяет писать безопасные программы. Также это важно для решения задач параллельного программирования. При необходимости переменные могут быть изменяемыми. Давайте рассмотрим преимущества того и другого подхода.

Поведение неизменяемых переменных напоминает поведение константы. Приведём пример использования этого типа переменной. Давайте создадим новый проект. Назовём его variables: cargo new --bin variables.

Потом перейдите в созданную папку проекта variables и отредактируйте исходный код следующим образом:

Filename: 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]: re-assignment of 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;
  |     ^^^^^ re-assignment of immutable variable

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

Очень важно получить сообщение об ошибке такого рода на этапе компиляции. В других языках программирования такую ошибку бывает трудно отыскать.

Компилятор Rust гарантирует, что ошибки такого рода будут выявлены. Ещё одно преимущество такого кода - такой код проще в изучении.

Неизменяемость - это, конечно, замечательная опция, но, иногда, требуется иное поведение переменной. Сделать переменную изменяемой, при желании, очень просто. Достаточно добавить ключевое слово mut при декларировании переменной. Также, явное указание этой особенности переменной повышает читаемость исходного кода.

Рассмотрим использование изменяемой переменой на практике. Пожалуйста, измените код программы src/main.rs следующим образом (обратите внимание, что мы просто добавили при декларировании переменной x ключевое слово mut):

Filename: 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)
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

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

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

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

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

  1. При объявлении констант нельзя использовать mut (логично по определению).
  2. При объявлении константы используется ключевое слово const, а при объявлении переменой let.
  3. При объявлении константы указание типа данных обязательно (для оптимизации).

Важной особенностью констант является область видимости, в которой можно их декларировать. Без ограничений.

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

Вот пример объявления костанты MAX_POINTS. Для объявления констант рекомендуется использовать заглавные буквы.

const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("MAX_POINTS is: {}", MAX_POINTS);
}

Чтобы лучше понять что же такое константы, пожалуйста скомпилируйте следующий код:

fn main() {
    println!("MAX_POINTS is: {}", MAX_POINTS);
    const MAX_POINTS: u32 = 100_000;
    print();
}

fn print(){
  println!("MAX_POINTS is: {}", MAX_POINTS);
}
const MAX_POINTS: u32 = 200_000;

Константы доступны в своей области видимости. Также они могут скрываться одноименными константами во вложенной области видимости. Константы доступны в любом месте области видимости.

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

Многократное использование одноимённых переменных

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

Вот как это выглядит в коде программы:

Filename: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

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

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

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


# #![allow(unused_variables)]
#fn main() {
let spaces = "   ";
let spaces = spaces.len();
#}

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

Пожалуйста проверьте это утверждение на практике:

Filename: src/main.rs

fn main() {
    let x = "_";
    println!("The value of x is: {}", x);
    let x = x.len();
    println!("The value of x is: {}", x);
    let x = "Привет!";
    println!("The value of x is: {}", x);
    let x: u32 = x.len() as u32;
    println!("The value of x is: {}", x);
    let x = "Привет!";
    println!("The value of x is: {}", x);
    let x: f32 = 3.45;
    println!("The value of x is: {}", x);
}

Такая техника описания переменных даёт возможность использовать одно имя переменной для работы с разными типами данных. Изменяемая переменная не может обладать таким свойством, т.е компиляция:

fn main() {
  let mut x = "_";
  println!("The value of x is: {}", x);
  spaces = x.len();
  println!("The value of x is: {}", x);

}

или

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

}

приведёт к ошибке:

```text
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected &str, found usize
  |
  = note: expected type `&str`
             found type `usize`

Пожалуйста, проверьте как работает этот код! Поэкспериментируйте над использованием уже знакомых вам типов данных (строками, целыми и дробными числами)! Обратите внимание, что компилятор сам подставляет типы данных при инициализации переменных.

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