Variables en Rust
Переменные в Rust: иммутабельность, mut и типы данных
Переменная — это именованная ячейка в памяти. Как ящик с наклейкой. В Rust есть особенность, удивляющая всех новичков: по умолчанию переменные нельзя менять!
Иммутабельность: зачем запрещать изменения?
Аналогия: Ты написал рецепт торта и положил в файлик. Если кто угодно может изменить — добавить соль вместо сахара — торт испорчен. Лучше зафиксировать рецепт: «Этот рецепт окончательный!»
Изменяемые данные — частая причина ошибок. Особенно в многопоточных программах: два потока меняют одно значение одновременно — хаос. Rust говорит: «По умолчанию — не трогай. Хочешь изменять — скажи явно!»
let — создаём переменную
fn main() {
let x = 5; // неизменяемая переменная
println!("x = {}", x); // x = 5
// x = 10; // ОШИБКА КОМПИЛЯЦИИ!
// error[E0384]: cannot assign twice to immutable variable `x`
}
mut — разрешаем изменения
fn main() {
let mut score = 0; // mut = mutable (изменяемый)
println!("Начало: {}", score);
score = score + 10; // теперь можно!
println!("После уровня 1: {}", score); // 10
score += 20; // сокращение: score = score + 20
println!("После уровня 2: {}", score); // 30
score *= 2;
println!("Финал: {}", score); // 60
// Все операции присваивания:
let mut n = 10;
n += 5; // 15
n -= 3; // 12
n *= 2; // 24
n /= 4; // 6
n %= 4; // 2
println!("n = {}", n);
}
Shadowing — «затенение» переменных
Можно создать новую переменную с тем же именем — это shadowing. Старая «прячется» за новой. Это не изменение — это создание новой!
fn main() {
let x = 5;
println!("x = {}", x); // 5
let x = x + 1; // новая переменная = 6
println!("x = {}", x); // 6
{
let x = x * 2; // новая в блоке = 12
println!("x внутри = {}", x); // 12
}
println!("x снаружи = {}", x); // 6 — вернулось!
// Крутая особенность: можно МЕНЯТЬ ТИП!
let spaces = " "; // &str
let spaces = spaces.len(); // usize — тип изменился!
println!("Длина: {}", spaces); // 3
// С mut это НЕЛЬЗЯ:
// let mut spaces = " ";
// spaces = spaces.len(); // error: mismatched types
}
Константы
const MAX_SCORE: u32 = 100_000; // ЗАГЛАВНЫЕ_БУКВЫ, тип обязателен
const PI: f64 = 3.14159265358979;
const SCHOOL: &str = "Гимназия №1";
fn main() {
println!("Max: {}", MAX_SCORE);
let seconds = 24 * 60 * 60;
println!("Секунд в сутках: {}", seconds);
// Разница: const — при компиляции, let — при выполнении
}
Целые числа
fn main() {
// i = signed (со знаком), u = unsigned (без знака)
let a: i8 = -128; // от -128 до 127
let b: i16 = -32_768;
let c: i32 = -2_000_000; // САМЫЙ ЧАСТЫЙ!
let d: i64 = -9_000_000_000;
let e: u8 = 255; // от 0 до 255
let f: u16 = 65_535;
let g: u32 = 4_294_967_295;
let h: usize = 42; // для индексов
println!("i32: {c}, u8: {e}");
// Разные системы счисления:
let hex = 0xff; // 255
let octal = 0o77; // 63
let binary = 0b1111_0000; // 240
let byte = b'A'; // 65
println!("hex={hex}, bin={binary}, byte={byte}");
println!("{} {} {} {} {}", 5+3, 10-4, 6*7, 15/4, 15%4);
}
Числа с плавающей точкой
fn main() {
let x: f32 = 3.14; // менее точное
let y: f64 = 3.14159265359; // РЕКОМЕНДУЕТСЯ всегда
let z = 2.0; // по умолчанию f64
let n = 2.0_f64;
println!("sqrt(2) = {:.6}", n.sqrt()); // 1.414213
println!("2^10 = {}", n.powi(10)); // 1024.0
println!("abs(-7.5) = {}", (-7.5_f64).abs());
println!("ceil(3.2) = {}", 3.2_f64.ceil()); // 4.0
println!("floor(3.8) = {}", 3.8_f64.floor()); // 3.0
// Осторожно с NaN:
let nan = f64::NAN;
println!("NaN == NaN: {}", nan == nan); // false! NaN особый
}
bool и char
fn main() {
let yes = true;
let no = false;
println!("И: {}", true && false); // false
println!("ИЛИ: {}", true || false); // true
println!("НЕ: {}", !true); // false
// char — один Unicode символ (одинарные кавычки!)
let letter = 'A';
let emoji = '😻'; // поддерживает Unicode!
let heart = '♥';
println!("{} {} {}", letter, emoji, heart);
println!("Код 'A': {}", letter as u32); // 65
println!("Заглавная? {}", letter.is_uppercase());
}
Строки: String и &str
fn main() {
// &str — строковый срез: ссылка, нельзя изменить
let hello: &str = "Привет, мир!";
// String — владеющая строка: в куче, можно изменять
let mut greeting = String::from("Привет");
greeting.push_str(", Rust!"); // добавляем строку
greeting.push('!'); // добавляем символ
println!("{greeting}"); // Привет, Rust!!
// Создание строк
let s1 = "строка".to_string();
let s2 = String::from("строка");
// Конкатенация через format!
let info = format!("{} — {} лет", "Алиса", 12);
println!("{info}");
// Методы
let text = " Привет, Rust! ";
println!("Trim: '{}'", text.trim());
println!("Верхний: {}", text.to_uppercase());
println!("Содержит Rust: {}", text.contains("Rust"));
let csv = "яблоко,банан,вишня";
let fruits: Vec<&str> = csv.split(',').collect();
println!("{:?}", fruits);
}
Вывод типов и приведение
fn main() {
// Rust угадывает типы
let x = 42; // i32
let y = 3.14; // f64
let flag = true; // bool
let ch = 'a'; // char
// Нужна явная подсказка:
let big: i64 = 9_999_999_999;
// Суффиксы:
let small = 100u8;
let precise = 3.14f32;
// Явное приведение типов — Rust не конвертирует автоматически!
let i = 42i32;
let f = i as f64; // i32 → f64
let u = 300u32 as u8; // обрезается: 300 % 256 = 44
println!("{i} → {f}, 300u32 as u8 = {u}");
}
Практика: дневник оценок
fn main() {
let name = "Вася";
let mut math = 4.0_f64;
let russian = 5.0_f64;
let english = 3.5_f64;
let avg = (math + russian + english) / 3.0;
println!("{}: мат={math}, рус={russian}, англ={english}", name);
println!("Средний: {:.2}", avg);
math = 5.0;
let new_avg = (math + russian + english) / 3.0;
println!("После пересдачи: {:.2}", new_avg);
let grade = if new_avg >= 4.5 { "Отлично!" }
else if new_avg >= 3.5 { "Хорошо" }
else { "Удовлетворительно" };
println!("Итог: {}", grade);
}
Типичные ошибки
❌ Изменение неизменяемой переменной
let x = 5;
x = 10; // error: cannot assign twice to immutable variable
// Исправление: let mut x = 5;
❌ Несовпадение типов — нет автоматического преобразования
let x: i32 = 5;
let y: f64 = x; // error: mismatched types
// Исправление: let y: f64 = x as f64;
❌ Переполнение в debug-режиме
let x: u8 = 255;
let y = x + 1; // panic: attempt to add with overflow
let y = x.wrapping_add(1); // 0 — безопасно
let y = x.saturating_add(1); // 255 — безопасно
Итого
let x = 5 — неизменяемая. let mut x = 5 — изменяемая. Shadowing — новая переменная с тем же именем, можно менять тип. const — константа, тип обязателен.
Типы: i32/u32 (целые), f64 (дробные), bool, char, String, &str.
Следующий урок: условия и циклы — как принимать решения и повторять действия.
Дополнительные примеры и практика
Числовые операции и приведение типов
fn main() {
// Деление целых чисел — результат тоже целое!
let a = 10i32;
let b = 3i32;
println!("{} / {} = {}", a, b, a / b); // 3 (не 3.33!)
println!("{} % {} = {}", a, b, a % b); // 1 (остаток)
// Для дробного деления нужны f64:
println!("{:.4}", 10.0_f64 / 3.0); // 3.3333
// Степень и корень:
let sq = 5i32.pow(2); // 25
let rt = 144.0_f64.sqrt(); // 12.0
println!("5² = {}, √144 = {}", sq, rt);
// Абсолютное значение:
println!("|−7| = {}", (-7i32).abs()); // 7
// Диапазоны типов:
println!("i32: {} до {}", i32::MIN, i32::MAX);
println!("u8: {} до {}", u8::MIN, u8::MAX);
println!("f64 epsilon: {}", f64::EPSILON);
}
Строки — продвинутые операции
fn main() {
let s = String::from("Привет, мир Rust!");
// Длина в байтах (не символах для Unicode!)
println!("Байт: {}", s.len());
// Проверки
println!("Пусто? {}", s.is_empty());
println!("Начинается с 'П': {}", s.starts_with('П'));
println!("Заканчивается на '!': {}", s.ends_with('!'));
println!("Содержит 'Rust': {}", s.contains("Rust"));
// Замена
let replaced = s.replace("мир", "вселенная");
println!("{}", replaced);
// Разбивка
let words: Vec<&str> = s.split_whitespace().collect();
println!("Слов: {}", words.len());
println!("{:?}", words);
// Итерация по символам (правильный способ для Unicode!)
let count = s.chars().count();
println!("Символов: {}", count);
// Повторение
let repeated = "abc ".repeat(3);
println!("{}", repeated); // abc abc abc
}
Полный пример: конвертер температур
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn fahrenheit_to_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
fn celsius_to_kelvin(c: f64) -> f64 {
c + 273.15
}
fn main() {
let temperatures_c = [0.0, 20.0, 37.0, 100.0, -40.0];
println!("{:>8} | {:>12} | {:>10}", "Цельсий", "Фаренгейт", "Кельвин");
println!("{}", "─".repeat(36));
for &t in &temperatures_c {
println!("{:>8.1} | {:>12.1} | {:>10.2}",
t,
celsius_to_fahrenheit(t),
celsius_to_kelvin(t)
);
}
// Тело человека
let body = 36.6_f64;
println!("\nТемпература тела: {}°C = {:.1}°F = {:.2}K",
body,
celsius_to_fahrenheit(body),
celsius_to_kelvin(body)
);
}
Shadowing для обработки ввода
use std::io;
fn main() {
// Shadowing используется для преобразования типов ввода
println!("Введите число:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Ошибка");
let input = input.trim(); // &str (убрали пробелы)
let input: i32 = input.parse() // i32 (тот же тип!)
.unwrap_or(0); // 0 если ошибка
println!("Вы ввели: {}", input);
println!("Удвоенное: {}", input * 2);
println!("Квадрат: {}", input * input);
}
Итого (расширенное)
Rust — язык с явными, строгими типами. Это кажется сложным поначалу, но защищает от целого класса ошибок. Компилятор — твой друг: он скажет об ошибке до запуска, не во время работы на сервере!
Главное запомнить:
let— неизменяемая переменная (безопаснее)let mut— изменяемая (явно объявляем намерение)- Shadowing — новая переменная с тем же именем (не то же что mut)
- Числа: i32 для целых по умолчанию, f64 для дробных
- Строки: &str для литералов, String для изменяемых данных
- Приведение типов — только явно через
as
Дополнительные примеры и упражнения
Работа с коллекциями
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
fn main() {
// HashMap — ассоциативный массив
let mut scores: HashMap> = HashMap::new();
scores.entry("Алиса".to_string()).or_insert_with(Vec::new).push(5);
scores.entry("Алиса".to_string()).or_insert_with(Vec::new).push(4);
scores.entry("Боря".to_string()).or_insert_with(Vec::new).push(3);
scores.entry("Боря".to_string()).or_insert_with(Vec::new).push(4);
for (name, grades) in &scores {
let avg: f64 = grades.iter().sum::() as f64 / grades.len() as f64;
println!("{}: avg={:.2} grades={:?}", name, avg, grades);
}
// HashSet — уникальные значения
let mut seen: HashSet = HashSet::new();
let data = vec![1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
let mut unique = Vec::new();
for &n in &data {
if seen.insert(n) {
unique.push(n);
}
}
println!("Уникальные: {:?}", unique);
// Пересечение и объединение множеств
let set_a: HashSet<_> = [1, 2, 3, 4, 5].iter().collect();
let set_b: HashSet<_> = [3, 4, 5, 6, 7].iter().collect();
let intersection: Vec<_> = set_a.intersection(&set_b).collect();
let union: Vec<_> = set_a.union(&set_b).collect();
println!("Пересечение: {:?}", intersection);
println!("Объединение: {:?}", union);
// VecDeque — двусторонняя очередь
let mut queue: VecDeque = VecDeque::new();
queue.push_back(1);
queue.push_back(2);
queue.push_back(3);
queue.push_front(0);
println!("Очередь: {:?}", queue);
println!("Извлекли спереди: {:?}", queue.pop_front());
}
Обработка ошибок и ? оператор
use std::num::ParseIntError;
use std::fmt;
#[derive(Debug)]
enum AppError {
ParseError(ParseIntError),
ValidationError(String),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::ParseError(e) => write!(f, "Ошибка парсинга: {}", e),
AppError::ValidationError(msg) => write!(f, "Ошибка валидации: {}", msg),
AppError::NotFound(key) => write!(f, "Не найдено: {}", key),
}
}
}
impl From for AppError {
fn from(e: ParseIntError) -> Self { AppError::ParseError(e) }
}
fn parse_positive(s: &str) -> Result {
let n: i32 = s.trim().parse()?; // ? преобразует ParseIntError через From
if n <= 0 {
return Err(AppError::ValidationError(format!("{} не положительное", n)));
}
Ok(n as u32)
}
fn find_square_root(n: &str) -> Result {
let num = parse_positive(n)?;
Ok((num as f64).sqrt())
}
fn main() {
let inputs = ["25", "-5", "abc", "100", "0"];
for input in inputs {
match find_square_root(input) {
Ok(r) => println!("√{} = {:.4}", input, r),
Err(e) => println!("Ошибка для '{}': {}", input, e),
}
}
}
Итераторы — ленивые вычисления
fn main() {
let numbers = vec![1i32, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Цепочка итераторов — ленивая, выполняется при collect/sum/etc
let result: Vec = numbers.iter()
.filter(|&&n| n % 2 == 0) // только чётные: 2,4,6,8,10
.map(|&n| n * n) // квадраты: 4,16,36,64,100
.filter(|&n| n > 20) // > 20: 36,64,100
.map(|n| format!("{}²", (n as f64).sqrt() as i32)) // "6²", "8²", "10²"
.collect();
println!("{:?}", result);
// zip — объединяем два итератора
let names = vec!["Алиса", "Боря", "Вера"];
let grades = vec![4.8, 3.9, 4.5];
let combined: Vec<_> = names.iter().zip(grades.iter()).collect();
for (name, grade) in &combined {
println!("{}: {:.1}", name, grade);
}
// enumerate + zip
let numbered: Vec<_> = names.iter()
.zip(grades.iter())
.enumerate()
.map(|(i, (n, g))| format!("{}. {}: {}", i+1, n, g))
.collect();
println!("{:?}", numbered);
// scan — промежуточные результаты (как fold, но возвращает итератор)
let running_sum: Vec = (1..=5)
.scan(0, |acc, x| { *acc += x; Some(*acc) })
.collect();
println!("Нарастающая сумма: {:?}", running_sum); // [1, 3, 6, 10, 15]
// flat_map — flatten + map
let words = vec!["hello world", "foo bar baz"];
let all_words: Vec<&str> = words.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", all_words);
// take и skip
let first_5: Vec = (1..).take(5).collect();
let skip_5: Vec = (1..=10).skip(5).collect();
println!("take(5): {:?}", first_5);
println!("skip(5): {:?}", skip_5);
}
Полный проект: студенческая база данных
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct Student {
name: String,
class: String,
grades: HashMap,
}
impl Student {
fn new(name: &str, class: &str) -> Self {
Student {
name: name.to_string(),
class: class.to_string(),
grades: HashMap::new(),
}
}
fn add_grade(&mut self, subject: &str, grade: f64) {
self.grades.insert(subject.to_string(), grade);
}
fn gpa(&self) -> f64 {
if self.grades.is_empty() { return 0.0; }
self.grades.values().sum::() / self.grades.len() as f64
}
fn is_excellent(&self) -> bool {
self.gpa() >= 4.5 && self.grades.values().all(|&g| g >= 4.0)
}
}
struct Database {
students: Vec,
}
impl Database {
fn new() -> Self { Database { students: Vec::new() } }
fn add(&mut self, student: Student) { self.students.push(student); }
fn by_class(&self, class: &str) -> Vec<&Student> {
self.students.iter().filter(|s| s.class == class).collect()
}
fn top_students(&self, n: usize) -> Vec<&Student> {
let mut sorted: Vec<&Student> = self.students.iter().collect();
sorted.sort_by(|a, b| b.gpa().partial_cmp(&a.gpa()).unwrap());
sorted.into_iter().take(n).collect()
}
fn class_average(&self, class: &str) -> f64 {
let class_students: Vec<_> = self.by_class(class);
if class_students.is_empty() { return 0.0; }
class_students.iter().map(|s| s.gpa()).sum::() / class_students.len() as f64
}
}
fn main() {
let mut db = Database::new();
let mut alice = Student::new("Алиса", "6А");
alice.add_grade("Математика", 5.0);
alice.add_grade("Русский", 5.0);
alice.add_grade("Английский", 4.0);
let mut bob = Student::new("Боря", "6Б");
bob.add_grade("Математика", 4.0);
bob.add_grade("Русский", 3.0);
bob.add_grade("Английский", 4.0);
let mut vera = Student::new("Вера", "6А");
vera.add_grade("Математика", 5.0);
vera.add_grade("Русский", 5.0);
vera.add_grade("Английский", 5.0);
db.add(alice);
db.add(bob);
db.add(vera);
println!("=== Класс 6А ===");
for s in db.by_class("6А") {
println!("{}: GPA={:.2} Отличник?{}", s.name, s.gpa(), s.is_excellent());
}
println!("\n=== Топ 2 студента ===");
for s in db.top_students(2) {
println!("{} ({}): {:.2}", s.name, s.class, s.gpa());
}
println!("\nСредний 6А: {:.2}", db.class_average("6А"));
println!("Средний 6Б: {:.2}", db.class_average("6Б"));
}
Советы для дальнейшего обучения
- The Rust Book — официальная книга, доступна на doc.rust-lang.org/book
- Rustlings — интерактивные упражнения:
cargo install rustlings - Rust by Example — примеры кода: doc.rust-lang.org/rust-by-example
- Exercism.io — задачи с ментором по треку Rust
- Crates.io — репозиторий пакетов для Rust