Переменные и мутабельность
В этой главе мы изучим одну из ключевых особенностей 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:
Объявление мутабельной переменной
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
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let mut sum = 0;
let mut product = 1;
for number in numbers {
sum += number; // Накапливаем сумму
product *= number; // Накапливаем произведение
}
println!("Сумма: {}", sum);
println!("Произведение: {}", product);
}
Вывод:
Сумма: 15
Произведение: 120
fn main() {
let mut message = String::new();
message.push_str("Привет, ");
message.push_str("мир!");
message.push('!');
println!("{}", message);
// Изменение существующего содержимого
message = message.replace("мир", "Rust");
println!("{}", message);
}
Вывод:
Привет, мир!!
Привет, Rust!!
Затенение переменных (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 config = "debug";
let result = {
let config = config.to_uppercase(); // Временное преобразование
let config = format!("MODE_{}", config);
config // Возвращаем из блока
};
println!("Исходная настройка: {}", config); // debug
println!("Обработанная: {}", result); // MODE_DEBUG
}
fn main() {
let data = " 123.45 ";
println!("Исходные данные: '{}'", data);
let data = data.trim(); // Удаляем пробелы
println!("После trim: '{}'", data);
let data: f64 = data.parse().unwrap(); // Парсим в число
println!("Как число: {}", data);
let data = data.round(); // Округляем
println!("Округлённое: {}", data);
let data = data as i32; // Приводим к целому
println!("Целое число: {}", data);
}
Вывод:
Исходные данные: ' 123.45 '
После trim: '123.45'
Как число: 123.45
Округлённое: 123
Целое число: 123
Области видимости и время жизни переменных
Блочные области видимости
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
// Статическая переменная с фиксированным адресом в памяти
static GLOBAL_COUNTER: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
static APP_VERSION: &str = "1.0.0";
// Мутабельная статическая переменная (небезопасно!)
static mut GLOBAL_STATE: i32 = 0;
fn main() {
println!("Версия приложения: {}", APP_VERSION);
// Безопасная работа с атомарным счётчиком
let old_value = GLOBAL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
println!("Счётчик: {} -> {}", old_value, old_value + 1);
// Небезопасная работа с мутабельной статической переменной
unsafe {
GLOBAL_STATE += 1;
println!("Глобальное состояние: {}", GLOBAL_STATE);
}
}
Особенности статических переменных:
- Имеют фиксированный адрес в памяти
- Время жизни = время жизни программы
- Мутабельные статические переменные требуют
unsafe
| Аспект | Неизменяемые переменные | Константы | Статические переменные |
|---|---|---|---|
| Ключевое слово | let | const | static |
| Мутабельность | let mut доступно | Всегда неизменяемы | static mut (unsafe) |
| Тип | Может быть выведен | Должен быть указан | Должен быть указан |
| Вычисление | Во время выполнения | Во время компиляции | Во время компиляции |
| Затенение | Разрешено | Запрещено | Запрещено |
| Область видимости | Блочная | Любая | Глобальная или модульная |
| Память | Стек/куча | Встраивается в код | Фиксированный адрес |
const COMPILE_TIME: i32 = 42;
static PROGRAM_LIFETIME: i32 = 42;
fn main() {
let runtime = 42;
let mut mutable = 42;
println!("Константа: {}", COMPILE_TIME);
println!("Статическая: {}", PROGRAM_LIFETIME);
println!("Время выполнения: {}", runtime);
println!("Изменяемая: {}", mutable);
mutable += 1; // ✅ Работает
// COMPILE_TIME += 1; // ❌ Ошибка компиляции
// PROGRAM_LIFETIME += 1; // ❌ Ошибка компиляции
}
Деструктуризация при объявлении
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 и их особенности.
Практические задания
-
Создайте программу-калькулятор, которая принимает два числа и выполняет над ними арифметические операции, используя мутабельные переменные для накопления результатов
-
Реализуйте функцию обработки строки, которая использует затенение для последовательного преобразования: удаление пробелов → преобразование в верхний регистр → подсчёт символов
-
Создайте программу с разными областями видимости, демонстрирующую, как переменные становятся недоступными при выходе из блока
-
Напишите пример, показывающий разницу между константами, статическими переменными и обычными переменными
Вопросы для самопроверки
- Почему переменные в Rust неизменяемы по умолчанию?
- В чём разница между затенением и мутабельностью?
- Когда следует использовать
const, а когдаstatic? - Что происходит с переменной при выходе из области видимости?