Перейти к основному содержимому

Переменные и мутабельность

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

Основы работы с переменными

Объявление переменных

В Rust переменные объявляются с помощью ключевого слова let:

Базовое объявление переменных
fn main() {
let x = 5;
println!("Значение x: {}", x);
}
По умолчанию неизменяемы

Все переменные в Rust неизменяемы по умолчанию! Это означает, что после присвоения значения его нельзя изменить.

Попытка изменения неизменяемой переменной

❌ Этот код не скомпилируется
fn main() {
let x = 5;
println!("Значение x: {}", x);

x = 6; // Ошибка компиляции!
println!("Новое значение x: {}", x);
}

Ошибка компилятора:

error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:5:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("Значение x: {}", x);
4 |
5 | x = 6;
| ^^^^^ cannot assign twice to immutable variable

Мутабельные переменные

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

Объявление мутабельной переменной

✅ Правильное использование mut
fn main() {
let mut x = 5;
println!("Значение x: {}", x);

x = 6; // Теперь это работает!
println!("Новое значение x: {}", x);
}

Вывод:

Значение x: 5
Новое значение x: 6

Практические примеры

Простой счётчик
fn main() {
let mut counter = 0;

println!("Начальное значение: {}", counter);

counter += 1;
println!("После увеличения: {}", counter);

counter *= 2;
println!("После удвоения: {}", counter);

counter -= 1;
println!("Финальное значение: {}", counter);
}

Вывод:

Начальное значение: 0
После увеличения: 1
После удвоения: 2
Финальное значение: 1

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

Затенение — это создание новой переменной с тем же именем, что "перекрывает" предыдущую:

Основы затенения

Простое затенение
fn main() {
let x = 5;
println!("x = {}", x);

let x = x + 1; // Новая переменная с тем же именем
println!("x = {}", x);

{
let x = x * 2; // Затенение в блоке
println!("x внутри блока = {}", x);
}

println!("x снова = {}", x);
}

Вывод:

x = 5
x = 6
x внутри блока = 12
x снова = 6

Затенение vs мутабельность

🔄 Затенение (Shadowing)

Затенение - создание новой переменной
fn main() {
let x = 5;
let x = "hello"; // Новая переменная, другой тип!
let x = x.len(); // Ещё одна новая переменная
println!("{}", x); // 5
}

Особенности:

  • Создаёт новую переменную
  • Может менять тип
  • Исходная переменная остаётся неизменяемой

🔧 Мутабельность (Mutability)

❌ Мутабельность - изменение значения
fn main() {
let mut x = 5;
x = "hello";// ❌ Это невозможно! x имеет тип i32
x = 10;// ✅ Можно менять значение того же типа
println!("{}", x); // 10
}

Особенности:

  • Изменяет существующую переменную
  • Тип должен остаться тем же
  • Переменная остаётся мутабельной

Практические применения затенения

Последовательные преобразования типов
fn main() {
// Читаем строку от пользователя
let input = "42";
println!("Введённая строка: '{}'", input);

// Преобразуем в число
let input: i32 = input.parse().expect("Не число!");
println!("Как число: {}", input);

// Вычисляем квадрат
let input = input * input;
println!("Квадрат: {}", input);

// Преобразуем обратно в строку для форматирования
let input = format!("Результат: {}", input);
println!("{}", input);
}

Вывод:

Введённая строка: '42'
Как число: 42
Квадрат: 1764
Результат: 1764

Области видимости и время жизни переменных

Блочные области видимости

Переменные в разных блоках
fn main() {
let x = 1;
println!("x в main: {}", x);

{
let y = 2;
println!("y в блоке: {}", y);
println!("x в блоке: {}", x); // x доступна

{
let z = 3;
println!("z во вложенном блоке: {}", z);
println!("x и y во вложенном блоке: {} {}", x, y);
}

// println!("{}", z); // ❌ Ошибка! z недоступна здесь
}

// println!("{}", y); // ❌ Ошибка! y недоступна здесь
println!("x снова в main: {}", x);
}

Время жизни переменных

Когда переменные создаются и уничтожаются
fn main() {
println!("Начало main");

{
println!("Вход в блок");
let temp = String::from("Временная строка");
println!("Создана переменная: {}", temp);
println!("Выход из блока");
} // temp уничтожается здесь

// println!("{}", temp); // ❌ temp больше не существует

println!("Конец main");
}

Константы vs неизменяемые переменные

В Rust есть разница между константами и неизменяемыми переменными:

Объявление и использование констант
// Константы объявляются на уровне модуля
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.141592653589793;
const APP_NAME: &str = "My Rust App";

fn main() {
println!("Максимальные очки: {}", MAX_POINTS);
println!("Число π: {}", PI);
println!("Имя приложения: {}", APP_NAME);

// Константы можно использовать в вычислениях во время компиляции
const CIRCLE_AREA: f64 = PI * 10.0 * 10.0;
println!("Площадь круга с радиусом 10: {}", CIRCLE_AREA);
}

Особенности констант:

  • Всегда неизменяемы (нельзя использовать mut)
  • Тип должен быть указан явно
  • Могут быть объявлены в любой области видимости
  • Значение должно быть вычислимо во время компиляции
  • Именуются в UPPER_SNAKE_CASE

Деструктуризация при объявлении

Rust позволяет деструктурировать сложные типы данных при объявлении переменных:

Деструктуризация кортежей

Работа с кортежами
fn main() {
let point = (3, 4);
let (x, y) = point; // Деструктуризация кортежа

println!("Координаты: x={}, y={}", x, y);

// Игнорирование части значений
let triple = (1, 2, 3);
let (first, _, third) = triple; // Игнорируем средний элемент
println!("Первый: {}, третий: {}", first, third);

// Мутабельная деструктуризация
let mut coordinates = (0, 0);
let (mut x, mut y) = coordinates;
x += 10;
y += 20;
println!("Новые координаты: ({}, {})", x, y);
}

Деструктуризация массивов

Работа с массивами
fn main() {
let array = [1, 2, 3, 4, 5];

// Деструктуризация первых элементов
let [first, second, ..] = array;
println!("Первые два: {}, {}", first, second);

// Деструктуризация с остатком
let [head, tail @ ..] = array;
println!("Голова: {}, хвост: {:?}", head, tail);

// Полная деструктуризация
let [a, b, c, d, e] = array;
println!("Все элементы: {} {} {} {} {}", a, b, c, d, e);
}

Деструктуризация структур

Работа со структурами
struct Person {
name: String,
age: u32,
}

fn main() {
let person = Person {
name: String::from("Алиса"),
age: 30,
};

// Деструктуризация структуры
let Person { name, age } = person;
println!("Имя: {}, возраст: {}", name, age);

// Переименование при деструктуризации
let person2 = Person {
name: String::from("Боб"),
age: 25,
};

let Person { name: person_name, age: person_age } = person2;
println!("Человек: {}, лет: {}", person_name, person_age);
}

Лучшие практики работы с переменными

1. Предпочитайте неизменяемость

✅ Хорошо: неизменяемые переменные
fn calculate_area(radius: f64) -> f64 {
let pi = 3.141592653589793;
let area = pi * radius * radius;
area
}

fn main() {
let radius = 5.0;
let result = calculate_area(radius);
println!("Площадь круга: {}", result);
}
❌ Плохо: излишняя мутабельность
fn calculate_area_bad(radius: f64) -> f64 {
let mut pi = 3.141592653589793; // mut не нужен
let mut area = pi * radius * radius; // mut не нужен
area
}

2. Используйте описательные имена

✅ Хорошо: понятные имена
fn main() {
let user_input = "42";
let parsed_number: i32 = user_input.parse().unwrap();
let squared_result = parsed_number * parsed_number;

println!("Квадрат числа {} равен {}", parsed_number, squared_result);
}
❌ Плохо: неясные имена
fn main() {
let x = "42";
let y: i32 = x.parse().unwrap();
let z = y * y;

println!("Квадрат числа {} равен {}", y, z);
}

3. Используйте блоки для ограничения области видимости

✅ Хорошо: ограниченная область видимости
fn main() {
let final_result = {
let temp_calculation = 42 * 2;
let adjustment = 10;
temp_calculation + adjustment
}; // temp_calculation и adjustment недоступны здесь

println!("Результат: {}", final_result);
}

4. Используйте затенение для преобразования типов

✅ Хорошо: использование затенения
fn process_user_input(input: &str) -> i32 {
let input = input.trim(); // &str -> &str
let input: i32 = input.parse().unwrap(); // &str -> i32
let input = input.abs(); // i32 -> i32
input
}

Заключение

В этой главе мы изучили фундаментальные концепции работы с переменными в Rust:

Неизменяемость по умолчанию — переменные let неизменяемы ✅ Мутабельность — использование mut для изменяемых переменных ✅ Затенение переменных — создание новых переменных с тем же именем ✅ Области видимости — время жизни переменных в блоках ✅ Константы и статические переменные — различия и использование ✅ Деструктуризацию — извлечение значений из сложных типов ✅ Лучшие практики — как писать понятный и безопасный код

Понимание системы переменных в Rust — это основа для освоения более сложных концепций языка, таких как система владения и заимствования.

Что дальше?

В следующей главе: "Базовые типы данных" — мы изучим встроенные типы данных Rust и их особенности.


Практические задания

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

  2. Реализуйте функцию обработки строки, которая использует затенение для последовательного преобразования: удаление пробелов → преобразование в верхний регистр → подсчёт символов

  3. Создайте программу с разными областями видимости, демонстрирующую, как переменные становятся недоступными при выходе из блока

  4. Напишите пример, показывающий разницу между константами, статическими переменными и обычными переменными

Вопросы для самопроверки

  1. Почему переменные в Rust неизменяемы по умолчанию?
  2. В чём разница между затенением и мутабельностью?
  3. Когда следует использовать const, а когда static?
  4. Что происходит с переменной при выходе из области видимости?

Полезные ссылки