Основы синтаксиса, система типов и управление потоком выполнения
Основы синтаксиса, система типов и управление потоком выполнения
В большинстве языков программирования простая опечатка в типе данных или забытая ветка условий проявляется ночью в виде падения сервера с TypeError или NullPointerException. Rust устраняет этот класс багов ещё на этапе сборки проекта. На первый взгляд его синтаксис напоминает C++ или JavaScript, однако семантика кардинально отличается: здесь почти всё является выражением, переменные неизменяемы по умолчанию, а неявные преобразования типов строго запрещены.
Разберём фундаментальные строительные блоки языка, на которых строится вся надёжность backend-систем.
Переменные: неизменяемость и затенение (Shadowing)
По умолчанию все переменные в Rust неизменяемы (immutable). Когда вы связываете значение с именем через ключевое слово let, компилятор гарантирует, что это значение не будет случайно перезаписано из другой части функции.
fn main() {
let port = 8080;
// port = 8081; // Ошибка компиляции: cannot assign twice to immutable variable `port`
}
Если значение действительно должно меняться во времени (например, счётчик обработанных сетевых пакетов), его объявляют явно с модификатором mut:
let mut active_connections = 0;
active_connections += 1;
Для неизменяемых значений времени компиляции используют const. Константы всегда требуют явного указания типа и вычисляются до старта программы:
const MAX_PAYLOAD_SIZE_BYTES: usize = 10 * 1024 * 1024; // 10 MiB
Затенение (Shadowing)
В Rust часто применяется паттерн, которого нет в императивных языках в таком виде — затенение переменных. Вы можете объявить новую переменную с тем же именем, используя ключевое слово let. При этом старая переменная перестаёт быть доступной в текущей области видимости, а новая может иметь другой тип:
// Исходные данные в виде строки из HTTP-заголовка
let request_timeout = "30";
// Затенение: преобразуем строку в число, сохранив понятное имя переменной
let request_timeout: u64 = request_timeout.parse().expect("Некорректное число");
// Ещё одно затенение с пересчётом в миллисекунды
let request_timeout = request_timeout * 1000;
| Критерий | Мутабельность (mut) |
Затенение (let) |
|---|---|---|
| Изменение типа | Запрещено | Разрешено |
| Изменение неизменяемости | Переменная остаётся мутабельной | Новая переменная снова неизменяема |
| Выделение нового связывания | Нет (перезапись того же места) | Да (создаётся новое имя в стеке) |
Система типов: абсолютная строгость
Rust — язык со статической сильной типизацией. Компилятор умеет выводить типы в большинстве случаев, но не производит неявных приведений. Число 42 типа i32 нельзя автоматически передать туда, где ожидается i64 или f64.
Скалярные типы
Скалярные типы представляют единичное значение:
- Целые числа:
- Знаковые:
i8,i16,i32,i64,i128,isize. - Беззнаковые:
u8,u16,u32,u64,u128,usize. - Тип по умолчанию для целых чисел —
i32. - Типы
usizeиisizeзависят от архитектуры процессора (4 байта на 32-битных и 8 байт на 64-битных системах) и используются для индексации коллекций.
- Знаковые:
- Числа с плавающей точкой:
f32иf64(стандарт IEEE-754). По умолчанию используетсяf64. - Логический тип:
bool(trueилиfalse). - Символьный тип:
char— представляет Unicode-символ (Unicode Scalar Value) и занимает 4 байта (а не 1 байт, какcharв C/C++):
let letter: char = 'R';
let emoji: char = '🦀'; // Полноценный 4-байтный Unicode scalar value
Составные типы (Compound Types)
- Кортежи (
Tuples): фиксированные наборы элементов разных типов, сгруппированные в единую структуру. Доступ к элементам осуществляется по индексу через точку:
let endpoint_info: (&str, u16, bool) = ("127.0.0.1", 8080, true);
let ip = endpoint_info.0;
let port = endpoint_info.1;
// Деструктуризация кортежа
let (host, port_num, is_tls) = endpoint_info;
Пустой кортеж () называется unit-типом (единичный тип). Он обозначает отсутствие полезного значения (аналог void в C/Java).
- Массивы (
Arrays): фиксированные наборы элементов одного типа, размещаемые целиком в стеке. Размер массива является частью его типа:
let allowed_ports: [u16; 3] = [80, 443, 8080];
let default_buffer: [u8; 1024] = [0; 1024]; // 1024 нуля
Выражения против инструкций (Expressions vs Statements)
В Rust синтаксис строится на фундаментальном различии между инструкциями (statements) и выражениями (expressions).
Инструкция (Statement) — это действие, которое выполняет операцию, но не возвращает значения. Объявление переменной через
let— это инструкция.Выражение (Expression) — вычисляет и возвращает результирующее значение. Выражением является математическая операция, вызов функции, блок кода
{}или конструкцияif.
Точка с запятой ; в конце строки превращает любое выражение в инструкцию, отбрасывая его значение и возвращая unit-тип ().
fn calculate_backoff(retry_count: u32) -> u64 {
// Блок кода вычисляет значение и возвращает его без ключевого слова return:
let base_delay = 100;
base_delay * (2_u64.pow(retry_count)) // Хвостовое выражение: нет точки с запятой!
}
Если в последней строке блока поставить точку с запятой, компилятор посчитает, что функция возвращает (), и откажется компилировать код из-за несоответствия сигнатуре -> u64.
Управление потоком выполнения как выражения
Поскольку блоки в Rust являются выражениями, управляющие конструкции if, match и даже циклы loop могут возвращать значения прямо в переменные.
Условный оператор if
Конструкция if работает без круглых скобок вокруг условия, а само условие обязано быть строго типа bool (неявного приведения чисел или строк к true/false нет).
let status_code = 404;
// if как выражение: заменяет тернарный оператор
let status_category = if status_code >= 200 && status_code < 300 {
"Success"
} else if status_code >= 400 && status_code < 500 {
"Client Error"
} else {
"Other"
}; // Точка с запятой закрывает инструкцию let
Все ветки выражения
if / elseобязаны возвращать значения одного и того же типа. Нельзя в веткеifвернуть строку, а в веткеelse— число.
Циклы: loop, while и for
В Rust три механизма организации циклов:
loop— бесконечный цикл. Уникальная особенность: ключевое словоbreakможет возвращать значение наружу:
let mut retries = 0;
let auth_token = loop {
retries += 1;
if retries == 3 {
// break возвращает строку наружу в переменную auth_token
break "generated_jwt_token_xyz";
}
};
while— цикл с предусловием:
let mut queue_size = 5;
while queue_size > 0 {
queue_size -= 1;
}
for— безопасный перебор итераторов и диапазонов (ranges):
// 1..=5 — диапазон от 1 до 5 включительно
for attempt in 1..=5 {
// логика повторной отправки пакета
}
let headers = ["Content-Type", "Authorization", "Accept"];
for header in headers {
// перебор элементов массива
}
Метки циклов (Loop Labels)
Когда у вас есть вложенные циклы, директивы break и continue по умолчанию относятся только к самому внутреннему. Для явного управления внешними циклами используются метки, начинающиеся с одинарной кавычки ':
'workers: for worker_id in 0..4 {
'tasks: loop {
let has_work = false;
if !has_work {
// Прерываем именно внешний цикл workers
break 'workers;
}
}
}
Практический пример: валидация и тарификация входящего запроса
Соберём изученные концепции воедино: напишем функцию, которая проверяет параметры запроса к API, рассчитывает лимит и возвращает итоговый статус.
fn process_incoming_request(api_key: &str, requested_tier: u8, payload_kb: usize) -> (u16, u32) {
// 1. Проверка ключа (затенение для валидации)
let is_valid_key = api_key.starts_with("sk_live_");
if !is_valid_key {
return (401, 0); // Не авторизован
}
// 2. if-выражение для вычисления RPS-лимита по тарифу
let max_rps = if requested_tier == 1 {
100
} else if requested_tier == 2 {
500
} else {
50 // Бесплатный тариф по умолчанию
};
// 3. Вычисление итогового веса запроса через цикл и shadowing
let mut total_cost = 1;
let chunks = payload_kb / 64; // Каждые 64 Кб увеличивают вес
for _ in 0..chunks {
total_cost += 2;
}
// Хвостовое выражение кортежа (HTTP Status, Оставшийся лимит RPS)
(200, max_rps - total_cost)
}
fn main() {
let (status, remaining_quota) = process_incoming_request("sk_live_99812", 2, 128);
println!("Status: {}, Quota: {}", status, remaining_quota);
}
В этом примере компилятор строго проверил типы всех чисел, гарантировал безопасность работы с кортежами и потребовал возврата точных типов из каждой ветки условий. В следующей главе мы перейдём к центральной механике языка — системе владения и ссылкам.