В отличие от Go или Python, где сборкой часто занимаются сторонние инструменты, в Rust инструмент сборки и пакетный менеджер — cargo — часть экосистемы с первого дня. Он ставится вместе с компилятором rustc через rustup и покрывает практически весь цикл: создание проекта, сборку, тесты, форматирование, публикацию пакетов.
$ cargo new hello_rust
$ cd hello_rust
$ cargo run
Compiling hello_rust v0.1.0
Finished dev [unoptimized + debuginfo] target(s)
Running `target/debug/hello_rust`
Hello, world!
Команда cargo new создаёт минимальный скелет: файл Cargo.toml с метаданными и зависимостями, и src/main.rs с точкой входа. Компиляция в debug-режиме быстрая, но код не оптимизирован — для продакшн-сборки используется cargo build --release, включающий LLVM-оптимизации, инлайнинг и удаление отладочной информации.
| Команда | Назначение |
|---|---|
cargo build | Компиляция без запуска (debug-профиль) |
cargo run | Компиляция + запуск |
cargo build --release | Оптимизированная сборка |
cargo check | Только проверка типов/borrow-checker без кодогенерации — быстрее build |
cargo test | Запуск тестов |
cargo clippy | Расширенный линтер |
cargo fmt | Форматирование кода |
cargo check вместо cargo build — он не генерирует машинный код, а только проверяет типы и заимствования, что в разы быстрее на крупных проектах.
По умолчанию все переменные в Rust неизменяемы (immutable) — это осознанный выбор дизайна языка, который сразу отсекает целый класс ошибок, связанных со случайной мутацией состояния. Для изменяемости требуется явное ключевое слово mut.
fn main() {
let x = 5;
// x = 6; // ошибка компиляции: cannot assign twice to immutable variable
let mut y = 5;
y = 6; // ок, y объявлена как mut
// Shadowing: новая переменная с тем же именем "затеняет" старую
let z = 5;
let z = z * 2; // z теперь 10, старое значение недоступно
let z = z.to_string(); // можно даже сменить тип!
println!("{z}");
}
Shadowing — не то же самое, что мутация. Каждый let создаёт новую переменную (возможно, другого типа), а не изменяет существующую ячейку памяти. Это удобно для последовательных трансформаций одного логического значения без выдумывания новых имён вроде x_str, x_trimmed.
Константы (const) отличаются от immutable-переменных: они всегда должны иметь явную аннотацию типа, вычисляются на этапе компиляции и не могут быть mut в принципе.
const MAX_POINTS: u32 = 100_000;
Rust — статически типизированный язык со строгим выводом типов (type inference), но, в отличие от Python и во многом схоже с Go, требует, чтобы каждое значение имело известный на этапе компиляции тип.
| Категория | Типы | Пример |
|---|---|---|
| Целые со знаком | i8, i16, i32, i64, i128, isize | let x: i32 = -42; |
| Целые без знака | u8, u16, u32, u64, u128, usize | let x: u8 = 255; |
| С плавающей точкой | f32, f64 | let x: f64 = 2.0; |
| Булев | bool | let x: bool = true; |
| Символ | char (4 байта, Unicode scalar) | let x: char = 'z'; |
isize/usize — типы размером в машинное слово (4 байта на 32-битных платформах, 8 на 64-битных), используются прежде всего для индексации коллекций. По умолчанию нетипизированный целочисленный литерал становится i32, а литерал с плавающей точкой — f64.
fn main() {
let sum = 5 + 10; // i32 по умолчанию
let pi = 3.14; // f64 по умолчанию
let byte: u8 = 255;
// Переполнение: в debug-сборке — panic, в release — wrapping (обёртывание)
let overflowed = byte.wrapping_add(1); // явный wrapping = 0
let checked = byte.checked_add(1); // Option<u8> = None
println!("{sum} {pi} {overflowed:?} {checked:?}");
}
checked_*, saturating_* или wrapping_* методы вместо обычных операторов.
Кортеж (tuple) группирует значения разных типов фиксированной длины. Массив (array) хранит значения одного типа, также фиксированной длины, и располагается на стеке целиком.
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // деструктуризация
let five_hundred = tup.0; // доступ по индексу
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10]; // [0, 0, ..., 0] — 10 элементов
let first = arr[0];
// Выход за границы массива — panic во время выполнения (bounds check)
// let bad = arr[10]; // panicked at 'index out of bounds'
println!("{x} {y} {z} {five_hundred} {first}");
}
В отличие от C/C++, где выход за границы массива — undefined behavior, Rust всегда проверяет индекс во время выполнения (если он не вычислен на этапе компиляции) и вызывает panic. Это часть гарантий memory safety языка, оплачиваемая небольшим рантайм-оверхедом, который компилятор часто может убрать при доказуемой безопасности индекса.
Срез — это "вид" (view) на непрерывный участок памяти без владения им. Срез — fat pointer: указатель на начало данных + длина.
fn main() {
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..3]; // [2, 3], элементы с индексами 1 и 2
let s = String::from("hello world");
let hello: &str = &s[0..5];
let world: &str = &s[6..]; // до конца строки
println!("{slice:?} {hello} {world}");
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
Взяв срез &str, компилятор через borrow-checker гарантирует, что исходная строка не будет изменена или удалена, пока срез жив — в отличие от C, где указатель на подстроку легко превращается в dangling pointer после реаллокации буфера.
if в Rust — выражение, а не оператор: оно возвращает значение, поэтому его можно использовать в правой части let.
fn main() {
let number = 6;
let description = if number % 4 == 0 {
"divisible by 4"
} else if number % 3 == 0 {
"divisible by 3"
} else {
"not divisible"
};
println!("{description}");
// loop — бесконечный цикл, может возвращать значение через break
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // значение loop-выражения
}
};
// while — обычный условный цикл
let mut n = 3;
while n != 0 {
println!("{n}!");
n -= 1;
}
// for — идиоматичный обход итераторов, безопаснее и быстрее ручного индексирования
let arr = [10, 20, 30];
for element in arr.iter() {
println!("value: {element}");
}
for i in (1..4).rev() {
println!("{i}");
}
// Метки циклов для break/continue во вложенных циклах
'outer: for x in 0..5 {
for y in 0..5 {
if x * y > 6 {
break 'outer;
}
}
}
}
for x in collection.iter() ручной индексации по счётчику — компилятор часто устраняет проверки границ (bounds check elimination) при итерации, что делает такой код не медленнее, а порой быстрее C-style циклов.
println! — не функция, а макрос (отсюда !): форматная строка проверяется на этапе компиляции, а не в рантайме, как в C с printf. Ошибка в спецификаторе формата — ошибка компиляции, а не потенциальный краш в проде.
fn main() {
let name = "Rust";
let version = 1.75;
println!("Hello, {name}! Version: {version}"); // захват переменных из области видимости
println!("{} loves {}", "Ferris", name); // позиционные аргументы
println!("{0} again {0}, then {1}", "A", "B");
// Debug (:?) и Pretty-Debug (:#?) для составных типов
let v = vec![1, 2, 3];
println!("{v:?}"); // [1, 2, 3]
println!("{v:#?}"); // многострочный вывод
// Числовое форматирование
println!("{:5}", 42); // выравнивание по ширине
println!("{:05}", 42); // дополнение нулями: 00042
println!("{:.2}", 3.14159); // точность: 3.14
println!("{:x} {:b} {:o}", 255, 255, 255); // hex, binary, octal
// eprintln! — вывод в stderr; format! — сборка String без вывода
eprintln!("error: something failed");
let s: String = format!("{name} v{version}");
}
Чтобы кастомный тип поддерживал {:?}, достаточно вывести (derive) трейт Debug; для {} нужно вручную реализовать трейт Display — это заставляет явно решать, как тип должен выглядеть для пользователя, в отличие от Python, где __repr__/__str__ легко перепутать.
Тело функции — блок, последнее выражение которого (без ;) становится возвращаемым значением. Это единообразно работает и для блоков if, match, loop, вложенных {}.
fn plus_one(x: i32) -> i32 {
x + 1 // без точки с запятой — это возвращаемое значение
}
fn absolute(x: i32) -> i32 {
if x < 0 { -x } else { x }
}
fn main() {
println!("{} {}", plus_one(5), absolute(-7));
}
() (unit), и функция перестаёт возвращать ожидаемый тип — компилятор сообщит об ошибке несоответствия типов, но новичка это часто сбивает с толку.