🦀 Rust

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
← Précédent Introduction à Rust Suivant → Flux de contrôle en Rust