Перейти к основному содержимому

Типы данных и владение памятью

Разработчику Архитектору

О чём эта статья

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Главная тема Rust — владение, заимствование и время жизни ссылок. Примитивы, String / &str, let / mut, коллекции. Без этой главы async и Axum постоянно упираются в borrow checker.

Дальше: управлениеошибки Result · Работа с данными и структурами · Важные трейты и типы (таблицы Операции Vec / HashMap) · Справочник Rust


Типы данных и владение памятью

Rust — статически типизированный язык с сильной проверкой в safe-подмножестве; типобезопасность дополняется правилами владения памятью. Общие определения — типы данных, типизация.

Программирование на любом языке начинается с понимания того, как язык представляет информацию внутри программы. В Rust эта задача решается через строгую, но гибкую систему типов, которая сочетает безопасность, производительность и предсказуемость. Переменные и типы данных — это фундаментальные строительные блоки любой программы, и в Rust им уделяется особое внимание. Язык не просто позволяет хранить значения, он требует от программиста чётко определять, как эти значения устроены, сколько памяти они занимают и какие операции над ними допустимы.


Переменные как именованные контейнеры

В Rust переменная — это именованное место в памяти, предназначенное для хранения значения определённого типа.

Объявление переменной всегда начинается с ключевого слова let.

let x = 5;
// let x = 6; // Ошибка компиляции: cannot assign twice to immutable variable
println!("Значение x равно {}", x);

Разбор:

  • let x = 5; создает неизменяемую переменную с выводом типа (i32 по умолчанию).
  • Закомментированная строка показывает, что повторное присваивание без mut запрещено.
  • println! подставляет значение x в {} и печатает его в stdout.
  • Такой стиль делает изменение состояния явным и уменьшает скрытые побочные эффекты.

Это слово сигнализирует компилятору о создании нового имени, связанного с конкретным значением. По умолчанию все переменные в Rust являются неизменяемыми. Это означает, что после присвоения значения переменной его нельзя изменить. Такой подход поощряет функциональный стиль программирования, снижает количество ошибок, связанных с неожиданными изменениями состояния, и делает код более предсказуемым.

let number = 10; // Тип определяется как i32
let fraction = 2.5; // Тип определяется как f64
let message = "Hi"; // Тип определяется как &str

Разбор:

  • Здесь показан автоматический вывод типов компилятором Rust.
  • 10 интерпретируется как i32, 2.5 как f64, а строковый литерал как &str.
  • Разные типы позволяют выполнять только допустимые операции: смешивание без преобразования даст ошибку.
  • Такой вывод типов сокращает шум в коде при сохранении строгой типизации.

Если требуется изменять значение переменной, необходимо явно указать это, добавив ключевое слово mut после let. Например, запись let mut counter = 0; создаёт изменяемую переменную с начальным значением ноль. Такая явность — часть философии Rust: программа должна честно отражать свои намерения. Изменяемость — это осознанное решение, которое программист фиксирует в коде.

let mut counter = 0;
counter += 1;
counter += 5;
println!("Счётчик: {}", counter); // 6

Разбор:

  • mut разрешает изменять значение после первого присваивания.
  • Оператор += — составное присваивание: counter += 1 эквивалентно counter = counter + 1.
  • Без mut строки counter += ... не скомпилируются.
  • Явная изменяемость помогает отличать "константное состояние" от "рабочего счётчика".
let integer_variable: i32 = 42;
let floating_point: f64 = 3.14;
let text_string: &str = "Hello";
let character_symbol: char = 'A';

Разбор:

  • После : задается явная аннотация типа для каждой переменной.
  • i32, f64, &str, char демонстрируют разные категории данных: числа, строка-срез и символ.
  • Явные типы полезны в API-коде и учебных примерах, где важно сразу видеть намерение.
  • Компилятор проверяет соответствие литерала указанному типу уже на этапе компиляции.

Имя переменной в Rust следует правилам идентификаторов — оно может содержать буквы, цифры, символ подчёркивания и символы Unicode, но не может начинаться с цифры. Соглашения сообщества рекомендуют использовать стиль snake_case — все буквы строчные, слова разделяются символами подчёркивания. Это способствует единообразию кода и упрощает его чтение.

Нижнее подчеркивание в начале имени (_variable) служит сигналом компилятору и другим разработчикам о том, что значение этой переменной намеренно не используется. Это предотвращает генерацию предупреждений о неиспользуемых переменных, которые Rust выводит в случае, если созданная переменная остается без применения.

let user_id = 101;
let _unused_logger = "system_log";
let total_sum = 500;

Разбор:

  • user_id и total_sum соответствуют соглашению snake_case.
  • _unused_logger использует префикс _, чтобы подавить предупреждение о неиспользуемой переменной.
  • Числа получают целочисленный тип по контексту (обычно i32), строка — &str.
  • Имена выбраны семантически, что повышает читаемость бизнес-логики.

Константы объявляются с помощью ключевого слова const. Они всегда должны иметь явный тип данных и их значение известно на этапе компиляции. Константы доступны во всем модуле и не могут быть изменены после объявления.

Отличие констант от неизменяемых переменных заключается в области видимости и моменте инициализации. Константа должна быть инициализирована значением, известным компилятору, и распространяется на весь модуль.

const MAX_USERS: u32 = 1000;
const PI: f64 = 3.14159;

Разбор:

  • const объявляет значения, вычислимые на этапе компиляции.
  • Для констант тип обязателен (u32, f64), в отличие от переменных с выводом типа.
  • Имена принято писать в SCREAMING_SNAKE_CASE.
  • Эти значения встраиваются в код и не требуют изменяемого состояния.

Статические переменные (static) хранят свои значения в глобальной области памяти на протяжении всего времени выполнения программы. Они могут быть как изменяемыми, так и неизменяемыми, но доступ к ним требует особой осторожности из-за многопоточности. Статические переменные часто используются для хранения конфигураций или глобальных состояний.

static GLOBAL_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);

Разбор:

  • static создает глобальный объект с фиксированным адресом на все время жизни программы.
  • AtomicUsize выбран для потокобезопасного изменения счетчика без гонок данных.
  • ::new(0) задает начальное значение атомика.
  • Полный путь std::sync::atomic::... явно показывает модульную иерархию стандартной библиотеки.

Play ITЗагрузка интерактивного демо…


Типы данных — скалярные и составные

Rust разделяет типы данных на две большие категории: скалярные и составные. Скалярные типы представляют одно значение. Составные типы позволяют группировать несколько значений в одну единицу.


Скалярные типы

Скалярные типы в Rust включают целые числа, числа с плавающей запятой, логические значения и символы.

Целочисленные типы делятся на знаковые и беззнаковые.

Знаковые целые числа могут представлять как положительные, так и отрицательные значения. Беззнаковые — только неотрицательные.

Каждый тип имеет фиксированный размер в битах — 8, 16, 32, 64 или 128. Например, i32 — это знаковое 32-битное целое число, а u64 — беззнаковое 64-битное.

Существует также специальный тип isize и usize, размер которых зависит от архитектуры системы: 32 бита на 32-битной платформе и 64 бита на 64-битной. Эти типы часто используются для индексации и работы с размерами коллекций.

Код ITЗагрузка примера кода…

Разбор:

  • Блок демонстрирует семейства i* и u* с разным размером и диапазоном.
  • usize и isize зависят от архитектуры и часто применяются для индексов/смещений.
  • Диапазон 0..10 в цикле for включает 0 и исключает 10.
  • println! показывает типичный паттерн обхода последовательности без ручного управления памятью.

Числа с плавающей запятой в Rust представлены двумя типами: f32 и f64. Они соответствуют стандарту IEEE 754 и обеспечивают одинарную и двойную точность соответственно. По умолчанию, если тип не указан явно, компилятор выбирает f64, так как он обеспечивает лучшую точность при сопоставимой производительности на большинстве современных процессоров.

Код ITЗагрузка примера кода…

Разбор:

  • f32 и f64 различаются точностью и размером; суффикс _f32 закрепляет тип литерала.
  • Арифметика выполняется в типе операндов, поэтому смешивание типов требует явного приведения.
  • f64::INFINITY и f32::NAN показывают специальные IEEE-значения.
  • Пример подчеркивает, что вещественная математика не всегда точна побитово.

Логический тип в Rust называется bool и может принимать два значения: true и false. Он используется в условиях, циклах и логических выражениях. Логические операции, такие как конъюнкция (&&) и дизъюнкция (||), работают с этим типом и возвращают значение того же типа.

Код ITЗагрузка примера кода…

Разбор:

  • Все промежуточные результаты (and_result, or_result, not_result) имеют тип bool.
  • if is_active && !is_complete демонстрирует композицию логических операторов.
  • Операции сравнения (<, ==) возвращают bool, что удобно для условий и match.
  • Такой стиль обеспечивает читаемую бизнес-логику без числовых "флагов".

Символьный тип char в Rust представляет собой один символ Юникода. Он занимает четыре байта и способен хранить любые символы, включая эмодзи, акцентированные буквы и специальные символы. Это отличает Rust от языков, где символ ограничен ASCII или однобайтовым представлением.

Код ITЗагрузка примера кода…

Разбор:

  • char хранит один Unicode scalar value, поэтому поддерживает кириллицу, эмодзи и математические символы.
  • text.chars().next() итерирует по Unicode-символам строки, а не по байтам.
  • Сравнения символов ('A' > 'a') идут по кодовым точкам Unicode.
  • Приведение 'A' as u32 показывает числовой код символа (code point).

Составные типы

Составные типы объединяют несколько значений в одну структуру. В Rust есть два основных встроенных составных типа: кортежи и массивы.

Кортеж — это упорядоченная последовательность значений, каждое из которых может иметь собственный тип. Кортеж фиксированного размера создаётся путём перечисления значений в круглых скобках. Например, (500, 6.4, 'a') — это кортеж из трёх элементов: целого числа, числа с плавающей запятой и символа. Тип этого кортежа записывается как (i32, f64, char). Доступ к элементам кортежа осуществляется через декомпозицию (деструктуризацию) или по индексу с помощью точки и номера: tuple.0, tuple.1 и так далее. Кортежи удобны для временного группирования связанных данных, особенно когда нужно вернуть несколько значений из функции.

Код ITЗагрузка примера кода…

Разбор:

  • Кортеж (i32, f64, char) объединяет значения разных типов в фиксированной позиции.
  • Доступ mixed_tuple.0/.1/.2 работает по индексу поля, заданному на этапе компиляции.
  • Деструктуризация let (number, float_val, symbol) делает код выразительнее и безопаснее индексов.
  • () — unit-тип: "пустое значение", часто используется как маркер отсутствия полезного результата.
Сначала — теория (раздел 3 "Данные")
вектор.push(x) // Vec — динамический массив
элемент := вектор[i]

хеш.вставить(ключ, значение) // HashMap
значение := хеш.получить(ключ)

Разбор:

  • Этот блок описывает базовые операции над Vec и HashMap в псевдокоде.
  • push добавляет элемент в конец динамического массива, при необходимости расширяя емкость.
  • Доступ по индексу предполагает, что индекс валиден; в Rust для безопасного доступа используют и .get(i).
  • Вставка/получение по ключу иллюстрируют модель словаря "ключ -> значение".

Play ITЗагрузка интерактивного демо…

Код ITЗагрузка примера кода…

Разбор:

  • Vec::new() создает пустой динамический массив; push добавляет элементы в конец.
  • Индексация scores[1] быстрая, но паникует при неверном индексе.
  • HashMap::insert сохраняет пару ключ-значение; ключи здесь &str, значения i32.
  • get возвращает Option<&V>: безопасный доступ без паники, если ключа нет.

Массив — это упорядоченная коллекция элементов одного типа. В отличие от многих других языков, массивы в Rust имеют фиксированный размер, который определяется во время компиляции. Это означает, что массив не может расти или сокращаться во время выполнения программы. Объявление массива выглядит как список значений в квадратных скобках — [1, 2, 3, 4, 5]. Если все элементы одинаковы, можно использовать сокращённую форму: [3; 5] создаёт массив из пяти троек. Тип массива записывается как [T; N], где T — тип элемента, а N — количество элементов. Массивы хранятся в стеке, что делает их быстрыми и предсказуемыми по памяти, но ограничивает их применение случаями, где размер известен заранее и невелик.

Код ITЗагрузка примера кода…

Разбор:

  • numbers имеет тип [i32; 5]: фиксированный размер известен компилятору.
  • Индексация numbers[0] быстрая, но выход за границы приводит к panic в runtime.
  • let zeros = [0; 10] использует синтаксис повторения одного значения.
  • &numbers[1..4] создает срез (&[i32]) без копирования данных.

Play ITЗагрузка интерактивного демо…


Аннотации типов и вывод типов

Rust обладает мощной системой вывода типов. Это означает, что во многих случаях компилятор может автоматически определить тип переменной на основе присвоенного значения. Например, в строке let x = 5; компилятор выводит, что x имеет тип i32, так как это стандартный целочисленный тип по умолчанию. Однако программист всегда может указать тип явно, используя двоеточие после имени переменной: let x: i32 = 5;.

Явное указание типа полезно в ситуациях, когда значение не даёт однозначной информации о типе, или когда требуется уточнить намерения. Например, при вызове функции, которая принимает несколько возможных типов, аннотация помогает компилятору выбрать правильный вариант. Также аннотации делают код более читаемым, особенно для сложных структур данных.


Владение и время жизни в контексте переменных

правило: у каждого значения ровно один владелец

АЛГОРИТМ ИспользоватьСтроку()
s1 := новая_строка("привет")
s2 := s1 // владение переехало в s2; s1 недействительна
// s1.длина() → ошибка: значение перемещено
ссылка := заимствовать(s2) // читать можно, пока s2 владелец
конец // s2 вышла из области — память освобождена
КОНЕЦ

Разбор:

  • Псевдокод показывает ключевой принцип ownership: один владелец на значение.
  • Присваивание s2 := s1 моделирует move-семантику, а не глубокую копию.
  • заимствовать(s2) отражает доступ по ссылке без передачи владения.
  • Выход владельца из области видимости запускает освобождение ресурсов (drop) автоматически.
КонструкцияСмысл в Rust
Перемещение (s2 := s1)Нет копии кучи, старый владелец "гаснет"
&T (заимствование)Временный доступ без смены владельца
Конец областиАвтоматический drop без GC
let s1 = String::from("привет");
let s2 = s1; // владение перемещено в s2
// println!("{}", s1); // ошибка: value moved

let len = s2.len(); // читаем через владельца s2
println!("{} ({})", s2, len);

Разбор:

  • String хранит данные в куче, поэтому присваивание s2 = s1 — это move, не копия.
  • После move переменная s1 становится недействительной для использования.
  • s2.len() вызывает метод у текущего владельца строки.
  • При выходе s2 из области видимости память освобождается автоматически.
fn print_length(text: &str) {
println!("Длина: {}", text.len());
}

let message = String::from("Rust");
print_length(&message); // заимствование, владение остаётся у message
println!("Снова доступно: {}", message);

Разбор:

  • &str в параметре принимает срез строки — и String, и литерал.
  • &message передает ссылку без передачи владения.
  • Функция может читать данные, но не владеет ими.
  • После вызова message по-прежнему доступна в main.

Хотя тема владения выходит за рамки простого объявления переменных, она тесно связана с тем, как Rust управляет данными. Каждое значение в Rust имеет владельца — переменную, которая отвечает за его существование. Когда владелец выходит из области видимости, значение автоматически уничтожается. Это правило гарантирует отсутствие утечек памяти без необходимости сборщика мусора.

Для составных типов, содержащих данные на куче (например, строки String или векторы Vec<T>), это означает, что при присвоении одной переменной другой происходит передача владения, а не копирование данных. Исходная переменная становится недействительной. Такое поведение — часть системы безопасности Rust, предотвращающей использование уже освобождённой памяти.


Строки — два мира — str и String

В Rust существует два основных способа работы с текстом: строковый срез (&str) и владеющая строка (String). Эти типы отражают фундаментальный принцип языка — различие между данными, которые только ссылаются на память, и данными, которые управляют своей памятью.

Строковый срез &str — это неизменяемая последовательность байтов в кодировке UTF-8, хранящаяся где-то в памяти. Чаще всего он используется в виде строковых литералов, например "привет". Такие литералы размещаются в секции исполняемого файла и существуют на протяжении всей программы. Срезы не владеют данными, они лишь указывают на них. Это делает их лёгкими и быстрыми для передачи.

Тип String, напротив, представляет собой изменяемую, растущую строку, выделенную в куче. Он владеет своими данными, что позволяет добавлять, удалять или изменять содержимое. Создание строки происходит через функцию String::from("текст") или метод .to_string(). Например:

let mut s = String::from("Привет");
s.push_str(", мир!");

Разбор:

  • String::from(...) создает владеющую строку в куче.
  • mut обязателен, потому что push_str изменяет содержимое строки.
  • push_str добавляет строковый срез &str в конец текущей строки.
  • Пример показывает отличие String (изменяемый владелец) от &str (неизменяемый срез).

Этот код корректен, потому что s является изменяемой переменной типа String. Попытка изменить строковый срез приведёт к ошибке компиляции, так как срезы по своей природе неизменяемы.

Преобразование между &str и String происходит часто. Любой String можно превратить в &str с помощью взятия ссылки: &s. Обратное преобразование требует выделения памяти и копирования данных, что делается явно через String::from() или .to_owned().

Play ITЗагрузка интерактивного демо…


Пользовательские типы данных

Rust предоставляет мощные инструменты для создания собственных типов. Основные из них — структуры (struct) и перечисления (enum).

Структура объединяет несколько значений под одним именем. Каждое поле имеет своё имя и тип. Например:

struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

Разбор:

  • struct User определяет пользовательский тип с именованными полями.
  • String в полях означает владение текстовыми данными каждым экземпляром структуры.
  • Типы полей фиксируют контракт данных на уровне компилятора.
  • Такой тип удобно расширять методами через impl.

Этот тип описывает пользователя с четырьмя полями. Экземпляр создаётся синтаксисом инициализации структуры:

let user1 = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
active: true,
};

Разбор:

  • Экземпляр создается литералом структуры: каждому полю задается значение.
  • Для строк используется String::from, чтобы получить владеющий тип String.
  • user1 по умолчанию неизменяем, пока не добавлен mut.
  • Компилятор требует инициализировать все обязательные поля.

Поля структуры доступны через точку: user1.email.

Перечисления позволяют определить тип, который может быть одним из нескольких вариантов. Например:

enum IpAddr {
V4(String),
V6(String),
}

Разбор:

  • enum описывает тип-сумму: значение может быть либо V4, либо V6.
  • Оба варианта несут данные типа String.
  • Такой дизайн помогает сделать состояние явным вместо "магических" флагов.
  • Обычно далее это значение разбирают через match.

Здесь IpAddr может быть либо IPv4-адресом, либо IPv6-адресом, каждый из которых хранит строку. Более эффективно использовать встроенные целочисленные типы:

enum IpAddr {
V4(u8, u8, u8, u8),
V6(u16, u16, u16, u16, u16, u16, u16, u16),
}

Разбор:

  • Вариант V4 хранит 4 октета как u8, что точно соответствует формату IPv4.
  • V6 хранит 8 групп по u16, отражая структуру IPv6-адреса.
  • Такой подход убирает лишний парсинг строк и делает модель данных более строгой.
  • Типовая система не позволит случайно перепутать форматы адресов.

Такой подход экономит память и исключает недопустимые значения.

Особенно важен стандартный перечисляемый тип Option<T>, который выражает наличие или отсутствие значения:

enum Option<T> {
Some(T),
None,
}

Разбор:

  • Option<T> — обобщенный enum для "значение есть/значения нет".
  • Some(T) оборачивает реальное значение, None представляет его отсутствие.
  • Такой тип заменяет null-подход и вынуждает явно обработать оба сценария.
  • В реальном коде обычно используется match, if let, map, unwrap_or и похожие методы.

Option выражает отсутствие значения явно: компилятор требует обработать и Some, и None. Это снижает риск обращения к "пустому" значению, как при null-указателях в C-подобных языках.


Константы и статические переменные

Помимо обычных переменных, Rust поддерживает константы и статические переменные.

Константа объявляется с помощью ключевого слова const. Она всегда неизменяема, её значение должно быть известно во время компиляции, и она не имеет фиксированного адреса в памяти. Константы могут использоваться в любом месте программы и часто применяются для определения глобальных параметров:

const MAX_POINTS: u32 = 100_000;

Разбор:

  • Константа доступна в месте объявления как неизменяемое compile-time значение.
  • Подчеркивание в 100_000 улучшает читаемость числа.
  • Тип u32 задает явный диапазон и предотвращает неявные преобразования.
  • Хороший вариант для лимитов, настроек и порогов в бизнес-логике.

Имена констант пишутся заглавными буквами с подчёркиваниями.

Статическая переменная объявляется через static. В отличие от констант, она имеет фиксированный адрес в памяти и живёт всё время выполнения программы. Статические переменные могут быть изменяемыми, но работа с ними требует особых мер предосторожности, так как они нарушают правила владения. Изменяемые статические переменные используются редко и только в специфических сценариях, таких как низкоуровневые системы или встраиваемые устройства.


Область видимости и время жизни

Область видимости переменной в Rust начинается с точки её объявления и заканчивается в конце блока, в котором она объявлена. Блок — это любая часть кода, заключённая в фигурные скобки {}. Когда переменная выходит из области видимости, вызывается её деструктор (если он определён), и память освобождается.

Это поведение гарантирует, что ресурсы не утекают. Например, если переменная содержит файловый дескриптор или сетевое соединение, они будут корректно закрыты при выходе из блока.

Время жизни — это механизм, который компилятор использует для проверки, что ссылки всегда указывают на действительные данные. Хотя в простых случаях время жизни выводится автоматически, в сложных функциях может потребоваться явное указание. Например:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

Разбор:

  • 'a — параметр времени жизни, связывающий входные ссылки и возвращаемую ссылку.
  • Функция не создает новых строк, а возвращает одну из входных ссылок.
  • x.len() и y.len() сравнивают длину строк, после чего выбирается более длинная.
  • Аннотация lifetimes гарантирует, что возвращенная ссылка не переживет исходные данные.

Здесь 'a — это параметр времени жизни, который говорит компилятору, что возвращаемая ссылка будет жить столько же, сколько и обе входные ссылки. Это предотвращает возврат ссылки на данные, которые уже уничтожены.


Практические рекомендации

При работе с типами и переменными в Rust стоит придерживаться нескольких принципов:

— Используйте неизменяемые переменные по умолчанию. Делайте переменную изменяемой только тогда, когда это действительно необходимо. — Предпочитайте строковые срезы (&str) в качестве параметров функций, если не требуется владение строкой. Это делает функции более гибкими. — Используйте Option<T> вместо магических значений (например, -1 или "пустой" указатель) для обозначения отсутствия данных. — Объявляйте константы для значений, которые не меняются и используются в нескольких местах. — Избегайте преждевременной оптимизации. Доверяйте системе типов и компилятору — они помогут вам писать безопасный и эффективный код.


Сопоставление с образцом — декларативный доступ к данным

Rust предоставляет мощный механизм сопоставления с образцом (pattern matching), который позволяет декларативно извлекать данные из сложных структур. Этот механизм особенно эффективен при работе с перечислениями, кортежами и структурами.

Оператор match — центральный инструмент сопоставления. Он принимает значение и сравнивает его с набором образцов. Каждый образец описывает возможную форму данных. Например:

Код ITЗагрузка примера кода…

Разбор:

  • enum Message кодирует набор допустимых сообщений с разной формой данных.
  • match msg выполняет исчерпывающий разбор каждого варианта enum.
  • Деструктуризация (Move { x, y }, Write(text)) извлекает внутренние поля прямо в паттерне.
  • Компилятор не даст пропустить вариант, что делает обработку сообщений безопасной.

Каждая ветка match точно соответствует одному из вариантов перечисления. Компилятор проверяет, что все возможные случаи обработаны, что исключает ошибки, связанные с неполным анализом состояния.

Сопоставление работает не только с перечислениями. Оно применимо к кортежам:

let point = (3, 5);
match point {
(0, 0) => println!("Начало координат"),
(0, y) => println!("На оси Y: {}", y),
(x, 0) => println!("На оси X: {}", x),
(x, y) => println!("Произвольная точка: ({}, {})", x, y),
}

Разбор:

  • Сопоставление кортежа (x, y) позволяет проверять структуру и значения одновременно.
  • Ветки (0, y) и (x, 0) показывают частичное фиксирование значений в шаблоне.
  • Переменные, объявленные в паттерне, доступны только в соответствующей ветке.
  • Такой код обычно читается лучше, чем цепочка вложенных if.

И к структурам:

struct Point {
x: i32,
y: i32,
}

let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y } => println!("На оси Y: {}", y),
Point { x, y: 0 } => println!("На оси X: {}", x),
Point { x, y } => println!("Точка: ({}, {})", x, y),
}

Разбор:

  • Паттерны по структуре (Point { x: 0, y }) проверяют поле x и извлекают y.
  • Короткая форма Point { x, y } извлекает оба поля в одноименные переменные.
  • Это безопасный способ работать с данными структуры без ручного доступа через ..
  • match остается исчерпывающим для всех значений Point.

Во всех этих случаях сопоставление с образцом делает код выразительным и безопасным, избегая ручного доступа к полям или индексам.


Преобразования типов

Rust не выполняет неявных преобразований между типами. Это предотвращает неожиданные потери точности или изменения поведения программы. Все преобразования должны быть явными.

Для числовых типов используется метод .into() или функция as. Метод .into() работает через трейт Into и даёт типобезопасное преобразование без потери (для пар типов, где реализован Into). Например, u8 можно преобразовать в u16 без потерь:

let a: u8 = 5;
let b: u16 = a.into();

Разбор:

  • .into() выполняет безопасное преобразование, когда для пары типов реализован Into.
  • u8 -> u16 расширяет диапазон без потери информации.
  • Целевой тип (u16) задается контекстом слева, поэтому компилятор знает, что строить.
  • Этот стиль обычно предпочтительнее "жесткого" as, когда преобразование заведомо безопасно.

Однако обратное преобразование может привести к потере данных. В таких случаях Rust требует использования as, что сигнализирует о возможном риске:

let c: u16 = 1000;
let d: u8 = c as u8; // 1000 не помещается в u8 — результат 232

Разбор:

  • as выполняет приведение даже при потенциальной потере данных.
  • Значение 1000 не помещается в u8 (0..=255), поэтому происходит усечение по модулю 256.
  • Получившийся 232 демонстрирует, почему такие приведения требуют особой осторожности.
  • Для проверяемых преобразований в проде часто используют TryFrom.

Такой подход заставляет программиста осознанно принимать решение о преобразовании.

Для строковых типов преобразования также строго контролируются. Преобразование String в &str происходит через взятие ссылки. Обратное преобразование требует владения данными и выполняется через String::from() или .to_string().


Типы и память — стек и куча

Понимание того, где хранятся данные, помогает писать эффективный код. Rust разделяет память на стек и кучу.

Стек — это область памяти с быстрым доступом, работающая по принципу "последним пришёл — первым ушёл". Данные на стеке имеют фиксированный размер, известный во время компиляции. Скалярные типы, кортежи из скаляров, массивы фиксированного размера — всё это хранится на стеке.

Куча — это менее упорядоченная область памяти, используемая для данных переменного размера. Когда размер данных неизвестен заранее или может меняться, они размещаются в куче. Владеющие типы, такие как String и Vec<T>, хранят свои данные в куче, а в стеке оставляют только метаданные: указатель, длину и ёмкость.

Это разделение объясняет, почему присвоение String передаёт владение, а не копирует данные: копирование всей строки было бы дорогостоящим. Вместо этого Rust перемещает указатель, сохраняя производительность и безопасность.


Пример — работа с пользовательскими данными

Рассмотрим пример, объединяющий многие из рассмотренных концепций:

Код ITЗагрузка примера кода…

Разбор:

  • #[derive(Debug)] автоматически добавляет поддержку форматирования {:?} для отладочной печати.
  • Person и Contact демонстрируют связку struct + enum для моделирования доменных данных.
  • match contact безопасно извлекает данные из варианта Email или Phone.
  • String::from(...) создает владеющие строки, соответствующие правилам ownership.

Здесь: — Person — структура с владеющими полями. — Contact — перечисление, позволяющее хранить один из двух типов контактов. — #[derive(Debug)] автоматически реализует вывод структуры для отладки. — match безопасно извлекает данные из перечисления. — Все переменные неизменяемы, что соответствует рекомендациям.

Этот код демонстрирует, как система типов Rust обеспечивает ясность, безопасность и эффективность одновременно.