Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

Что вы узнаете: переменные, неизменяемые по умолчанию, явный mut, примитивные числовые типы в сравнении с int произвольной точности в Python, String и &str (самая трудная концепция на раннем этапе), форматирование строк и обязательные аннотации типов в Rust.

Сложность: 🟢 Начальный

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

# Python — всё изменяемо, типизация динамическая
count = 0          # Изменяемая, тип выводится как int
count = 5          # ✅ Работает
count = "hello"    # ✅ Работает — тип может меняться! (динамическая типизация)

# «Константы» — это только соглашение:
MAX_SIZE = 1024    # Ничто не мешает потом написать MAX_SIZE = 999

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

#![allow(unused)]
fn main() {
// Rust — по умолчанию неизменяемые, статически типизированные
let count = 0;           // Неизменяемая, тип выводится как i32
// count = 5;            // ❌ Ошибка компиляции: нельзя дважды присвоить значение неизменяемой переменной
// count = "hello";      // ❌ Ошибка компиляции: ожидалось целое число, найден &str

let mut count = 0;       // Явно изменяемая
count = 5;               // ✅ Работает
// count = "hello";      // ❌ Тип по-прежнему нельзя менять

const MAX_SIZE: usize = 1024; // Настоящая константа — гарантируется компилятором
}

Ключевой сдвиг мышления для разработчиков на Python

#![allow(unused)]
fn main() {
// Python: переменные — это метки, которые указывают на объекты
// Rust: переменные — это именованные места хранения, которые ВЛАДЕЮТ своими значениями

// Затенение (shadowing) — характерно для Rust, очень полезно
let input = "42";              // &str
let input = input.parse::<i32>().unwrap();  // Теперь i32 — новая переменная с тем же именем
let input = input * 2;         // Теперь 84 — ещё одна новая переменная

// В Python вы бы просто переприсвоили значение и потеряли старый тип:
input = "42"
input = int(input)   # То же имя, другой тип — Python тоже это позволяет
Но в Rust каждый `let` создаёт действительно новую привязку. Старая исчезает.
}

Практический пример: счётчик

# Версия на Python
class Counter:
    def __init__(self):
        self.value = 0
    
    def increment(self):
        self.value += 1
    
    def get_value(self):
        return self.value

c = Counter()
c.increment()
print(c.get_value())  # 1
// Версия на Rust
struct Counter {
    value: i64,
}

impl Counter {
    fn new() -> Self {
        Counter { value: 0 }
    }

    fn increment(&mut self) {     // &mut self — этот метод будет изменять объект
        self.value += 1;
    }

    fn get_value(&self) -> i64 {  // &self — этот метод только читает
        self.value
    }
}

fn main() {
    let mut c = Counter::new();   // Нужен `mut`, чтобы вызвать increment()
    c.increment();
    println!("{}", c.get_value()); // 1
}

Ключевое отличие: в Rust &mut self в сигнатуре метода сообщает и вам, и компилятору, что increment изменяет счётчик. В Python любой метод может изменить что угодно, и чтобы это понять, нужно читать код.


Сравнение примитивных типов

flowchart LR
    subgraph Python ["Типы Python"]
        PI["int<br/>(произвольная точность)"]
        PF["float<br/>(только 64 бита)"]
        PB["bool"]
        PS["str<br/>(Unicode)"]
    end
    subgraph Rust ["Типы Rust"]
        RI["i8 / i16 / i32 / i64 / i128<br/>u8 / u16 / u32 / u64 / u128"]
        RF["f32 / f64"]
        RB["bool"]
        RS["String / &str"]
    end
    PI -->|"фиксированный размер"| RI
    PF -->|"выбор точности"| RF
    PB -->|"то же"| RB
    PS -->|"владение или заимствование"| RS
    style Python fill:#ffeeba
    style Rust fill:#d4edda

Числовые типы

PythonRustПримечания
int (произвольная точность)i8, i16, i32, i64, i128, isizeЦелые числа в Rust имеют фиксированный размер
int (беззнаковые: отдельного типа нет)u8, u16, u32, u64, u128, usizeБеззнаковые типы указываются явно
float (64-бит, IEEE 754)f32, f64В Python есть только 64-битный float
boolboolТа же концепция
complexНет встроенного (используйте крейт num)Редко встречается в системном коде
# Python — один целочисленный тип, произвольная точность
x = 42                     # int — может расти до любого размера
big = 2 ** 1000            # По-прежнему работает — тысячи цифр
y = 3.14                   # float — всегда 64-битный
#![allow(unused)]
fn main() {
// Rust — явные размеры, переполнение — ошибка компиляции или выполнения
let x: i32 = 42;           // 32-битное знаковое целое
let y: f64 = 3.14;         // 64-битное число с плавающей точкой (аналог float в Python)
let big: i128 = 2_i128.pow(100); // Максимум 128 бит — произвольной точности нет
// Для произвольной точности используйте крейт `num-bigint`

// Подчёркивания для читаемости (как 1_000_000 в Python):
let million = 1_000_000;   // Тот же синтаксис, что и в Python!

// Синтаксис суффиксов типа:
let a = 42u8;              // u8
let b = 3.14f32;           // f32
}

Типы размера (важно!)

#![allow(unused)]
fn main() {
// usize и isize — целые размером с указатель, используются для индексации
let length: usize = vec![1, 2, 3].len();  // .len() возвращает usize
let index: usize = 0;                     // Индексы массивов всегда usize

// В Python len() возвращает int, и индексы тоже int — различий нет.
// В Rust смешивание i32 и usize требует явного преобразования:
let i: i32 = 5;
// let item = vec[i];    // ❌ Ошибка: ожидался usize, найден i32
let item = vec[i as usize]; // ✅ Явное преобразование
}

Вывод типов

#![allow(unused)]
fn main() {
// Rust выводит типы, но они ФИКСИРОВАННЫЕ — не динамические
let x = 42;          // Компилятор выводит i32 (целый тип по умолчанию)
let y = 3.14;        // Компилятор выводит f64 (вещественный тип по умолчанию)
let s = "hello";     // Компилятор выводит &str (срез строки)
let v = vec![1, 2];  // Компилятор выводит Vec<i32>

// Можно всегда указать тип явно:
let x: i64 = 42;
let y: f32 = 3.14;

// В отличие от Python, тип НИКОГДА не меняется после вывода:
let x = 42;
// x = "hello";      // ❌ Ошибка: ожидалось целое число, найден &str
}

Строковые типы: String и &str

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

Работа со строками в Python

# Python — один строковый тип, неизменяемый, с подсчётом ссылок
name = "Alice"          # str — неизменяемый, размещается в куче
greeting = f"Hello, {name}!"  # Форматирование через f-строку
chars = list(name)      # Преобразование в список символов
upper = name.upper()    # Возвращает новую строку (строки неизменяемы)

Строковые типы Rust

#![allow(unused)]
fn main() {
// В Rust ДВА строковых типа:

// 1. &str (срез строки) — заимствованный, неизменяемый, «вид» на данные строки
let name: &str = "Alice";           // Указывает на данные строки в бинарнике
                                     // Ближе всего к str в Python, но это ССЫЛКА

// 2. String (строка-владелец) — в куче, растёт, принадлежит владельцу
let mut greeting = String::from("Hello, ");  // Владеющая строка, её можно изменять
greeting.push_str(name);
greeting.push('!');
// greeting теперь "Hello, Alice!"
}

Когда что использовать?

#![allow(unused)]
fn main() {
// Думайте так:
// &str   = «Я смотрю на строку, которой владеет кто-то другой» (вид только для чтения)
// String = «Я владею этой строкой и могу её менять» (владеющие данные)

// Параметры функций: лучше &str (принимает оба типа)
fn greet(name: &str) -> String {          // принимает и &str, и &String
    format!("Hello, {}!", name)           // format! создаёт новый String
}

let s1 = "world";                         // литерал &str
let s2 = String::from("Rust");            // String

greet(s1);      // ✅ &str работает напрямую
greet(&s2);     // ✅ &String автоматически преобразуется в &str (Deref coercion)
}

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

# Строковые операции в Python
name = "alice"
upper = name.upper()               # "ALICE"
contains = "lic" in name           # True
parts = "a,b,c".split(",")         # ["a", "b", "c"]
joined = "-".join(["a", "b", "c"]) # "a-b-c"
stripped = "  hello  ".strip()     # "hello"
replaced = name.replace("a", "A") # "Alice"
#![allow(unused)]
fn main() {
// Аналоги в Rust
let name = "alice";
let upper = name.to_uppercase();           // String — новое выделение памяти
let contains = name.contains("lic");       // bool
let parts: Vec<&str> = "a,b,c".split(',').collect();  // Vec<&str>
let joined = ["a", "b", "c"].join("-");    // String
let stripped = "  hello  ".trim();         // &str — без выделения памяти!
let replaced = name.replace("a", "A");     // String

// Ключевая мысль: одни операции возвращают &str (без выделения памяти), другие — String.
// .trim() возвращает срез исходной строки — это эффективно!
// .to_uppercase() должен создать новый String — нужно выделение памяти.
}

Разработчикам на Python: думайте так

Python str     ≈ Rust &str     (вы в основном читаете строки)
Python str     ≈ Rust String   (когда нужно владеть или изменять)

Правило большого пальца:
- Параметры функций        → используйте &str (самый гибкий вариант)
- Поля структур            → используйте String (структура владеет своими данными)
- Возвращаемые значения    → используйте String (вызывающий код должен владеть ими)
- Строковые литералы       → автоматически &str

Вывод и форматирование строк

Базовый вывод

# Python
print("Hello, World!")
print("Имя:", name, "Возраст:", age)    # Через пробел
print(f"Имя: {name}, Возраст: {age}")   # f-строка
#![allow(unused)]
fn main() {
// Rust
println!("Hello, World!");
println!("Имя: {} Возраст: {}", name, age);    // Позиционные {}
println!("Имя: {name}, Возраст: {age}");       // Встроенные переменные (Rust 1.58+, как f-строки!)
}

Спецификаторы формата

# Форматирование в Python
print(f"{3.14159:.2f}")          # "3.14" — 2 знака после запятой
print(f"{42:05d}")               # "00042" — дополнение нулями
print(f"{255:#x}")               # "0xff" — шестнадцатеричный вид
print(f"{42:>10}")               # "        42" — выравнивание по правому краю
print(f"{'left':<10}|")          # "left      |" — выравнивание по левому краю
#![allow(unused)]
fn main() {
// Форматирование в Rust (очень похоже на Python!)
println!("{:.2}", 3.14159);         // "3.14" — 2 знака после запятой
println!("{:05}", 42);              // "00042" — дополнение нулями
println!("{:#x}", 255);             // "0xff" — шестнадцатеричный вид
println!("{:>10}", 42);             // "        42" — выравнивание по правому краю
println!("{:<10}|", "left");        // "left      |" — выравнивание по левому краю
}

Отладочный вывод

# Python — repr() и pprint
print(repr([1, 2, 3]))             # "[1, 2, 3]"
from pprint import pprint
pprint({"key": [1, 2, 3]})         # Красивый вывод
#![allow(unused)]
fn main() {
// Rust — {:?} и {:#?}
println!("{:?}", vec![1, 2, 3]);       // "[1, 2, 3]" — формат Debug
println!("{:#?}", vec![1, 2, 3]);      // Красивый формат Debug

// Чтобы типы можно было вывести, добавьте derive Debug:
#[derive(Debug)]
struct Point { x: f64, y: f64 }

let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p);                   // "Point { x: 1.0, y: 2.0 }"
println!("{p:?}");                     // То же самое, но со встроенным синтаксисом
}

Краткая справка

PythonRustПримечания
print(x)println!("{}", x) или println!("{x}")Формат Display
print(repr(x))println!("{:?}", x)Формат Debug
f"Hello {name}"format!("Hello {name}")Возвращает String
print(x, end="")print!("{x}")Без перевода строки (print! и println!)
print(x, file=sys.stderr)eprintln!("{x}")Вывод в stderr
sys.stdout.write(s)print!("{s}")Без перевода строки

Аннотации типов: необязательные и обязательные

Подсказки типов в Python (необязательные, не проверяются)

# Python — подсказки типов служат документацией, а не принудительной проверкой
def add(a: int, b: int) -> int:
    return a + b

add(1, 2)         # ✅
add("a", "b")     # ✅ Python не возражает — возвращает "ab"
add(1, "2")       # ✅ Пока не упадёт во время выполнения: TypeError

# Union-типы, Optional
def find(key: str) -> int | None:
    ...

# Обобщённые типы
def first(items: list[int]) -> int | None:
    return items[0] if items else None

# Псевдонимы типов
UserId = int
Mapping = dict[str, list[int]]

Объявления типов в Rust (обязательные, проверяются компилятором)

#![allow(unused)]
fn main() {
// Rust — типы проверяются всегда, без исключений.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

add(1, 2);         // ✅
// add("a", "b");  // ❌ Ошибка компиляции: ожидался i32, найден &str

// Необязательные значения используют Option<T>
fn find(key: &str) -> Option<i32> {
    // Возвращает Some(value) или None
    Some(42)
}

// Обобщённые типы
fn first(items: &[i32]) -> Option<i32> {
    items.first().copied()
}

// Псевдонимы типов
type UserId = i64;
type Mapping = HashMap<String, Vec<i32>>;
}

Ключевая мысль: в Python подсказки типов помогают вашей IDE и mypy, но на выполнение программы не влияют. В Rust типы — это и есть программа: компилятор использует их, чтобы гарантировать безопасность памяти, предотвратить гонки данных и исключить ошибки нулевых указателей.

📌 См. также: Гл. 6 — Перечисления и сопоставление с образцом показывает, как система типов Rust заменяет Union-типы Python и проверки isinstance().


Упражнения

🏋️ Упражнение: конвертер температуры (нажмите, чтобы раскрыть)

Задание: напишите функцию celsius_to_fahrenheit(c: f64) -> f64 и функцию classify(temp_f: f64) -> &'static str, которая по пороговым значениям возвращает "холодно", "умеренно" или "жарко". Выведите результат для 0, 20 и 35 градусов Цельсия. Используйте форматирование строк.

🔑 Решение
fn celsius_to_fahrenheit(c: f64) -> f64 {
    c * 9.0 / 5.0 + 32.0
}

fn classify(temp_f: f64) -> &'static str {
    if temp_f < 50.0 { "холодно" }
    else if temp_f < 77.0 { "умеренно" }
    else { "жарко" }
}

fn main() {
    for c in [0.0, 20.0, 35.0] {
        let f = celsius_to_fahrenheit(c);
        println!("{c:.1}°C = {f:.1}°F — {}", classify(f));
    }
}

Ключевой вывод: Rust требует явного f64 (нет неявного преобразования int → float), for перебирает массивы напрямую (без range()), а блоки if/else — это выражения.