Rust для начинающих
Rust для начинающих
Эта статья — входная точка в язык: она объясняет базовые конструкции на практических примерах и подготавливает к более сложным темам раздела. После неё удобно переходить в управляющие конструкции и циклы, важные трейты и типы, работа с данными и первая программа на Rust.
Rust представляет собой многопарадигмальный язык программирования общего назначения, который сочетает в себе высокую скорость выполнения кода с гарантиями безопасности памяти на этапе компиляции. Язык был разработан для решения классических проблем системного программирования, таких как утечки памяти, доступ к несуществующим данным и гонки потоков, без необходимости использования сборщика мусора в процессе выполнения программы. Основная цель создания Rust заключалась в обеспечении возможности написания надежного кода, который работает так же быстро, как C и C++, но исключает целый класс ошибок, характерных для этих языков.
Философия языка строится на трех фундаментальных принципах: безопасность памяти, параллелизм и продуктивность разработчика. Безопасность памяти достигается за счет уникальной системы управления памятью, которая проверяет корректность работы с данными еще до запуска программы. Параллелизм поддерживается на уровне языка через механизмы, которые запрещают создание небезопасных состояний при одновременном доступе нескольких потоков к одним и тем же ресурсам. Продуктивность обеспечивается мощной системой типов, продвинутым компилятором и богатым набором инструментов для разработки.
Система типов Rust позволяет выражать сложные инварианты на уровне сигнатур функций и структур данных. Компилятор анализирует код и выдает подробные сообщения об ошибках, которые не только указывают на проблему, но и предлагают конкретное решение. Это снижает количество времени, затрачиваемого на отладку, и повышает уверенность разработчика в корректности написанного кода. Язык поддерживает функциональное, объектно-ориентированное и императивное стили программирования, что делает его универсальным инструментом для широкого спектра задач.
Rust активно используется в создании операционных систем, браузеров, игровых движков, сетевых протоколов, блокчейн-платформ и инструментов для облачной инфраструктуры. Компании Mozilla, Google, Microsoft, Amazon и Facebook интегрируют компоненты на Rust в свои продукты для повышения надежности и безопасности критически важных систем. Язык имеет активное сообщество, которое регулярно выпускает обновления, расширяет стандартную библиотеку и создает новые инструменты для экосистемы.
Как проходить материал на практике
Чтобы статья принесла максимум пользы, разбейте изучение на короткие итерации:
- Прочитайте один блок теории.
- Скопируйте пример в локальный проект.
- Измените пример под свой сценарий.
- Спровоцируйте ошибку намеренно и разберите подсказку компилятора.
Такой ритм быстро формирует инженерную привычку "читать ошибки как документацию", а в Rust это критично важно для уверенного роста.
Система владения (Ownership)
Play ITЗагрузка интерактивного демо…
Система владения является центральным элементом архитектуры Rust и определяет правила работы с памятью в языке. Каждая переменная в Rust имеет единственного владельца, который отвечает за освобождение памяти после завершения действия переменной. Когда владелец выходит за пределы области видимости, компилятор автоматически вызывает деструктор и освобождает занимаемую память. Это устраняет необходимость в ручном управлении памятью и предотвращает ошибки, связанные с двойным удалением или использованием уже удаленной памяти.
Правила системы владения формулируются следующим образом. Во-первых, каждая ценность в Rust принадлежит одному владельцу. Во-вторых, в любой момент времени может существовать только один владелец ценности. В-третьих, когда владелец выходит из области видимости, ценность уничтожается. Эти правила гарантируют отсутствие утечек памяти и использование после освобождения без накладных расходов на сборку мусора в рантайме.
Перемещение ценности происходит при присваивании одной переменной другой. При этом исходная переменная перестает быть валидной, и доступ к ней невозможен. Например, передача аргумента функции вызывает перемещение значения, если тип не реализует трейт Copy. Это поведение отличается от копирования, которое создает независимую копию данных. Разработчик должен понимать этот механизм для правильного проектирования структуры данных и передачи параметров.
let string1 = String::from("hello");
let string2 = string1;
// string1 больше недоступна здесь, так как ownership перешел к string2
println!("{}", string2);
Разбор:
String::from("hello")создает строку в куче, а переменнаяstring1становится ее владельцем.- Присваивание
let string2 = string1;делает move, а не копию, потому чтоStringне реализуетCopy. - После move исходная переменная считается невалидной, и компилятор запрещает дальнейшее использование
string1. println!("{}", string2)работает, потому что владение уже уstring2, и доступ к данным безопасен.- Такой пример показывает базовое правило ownership: в каждый момент есть один владелец ресурса.
В приведенном примере переменная string1 содержит указатель на данные в куче. Присваивание string2 = string1 перемещает этот указатель, а не копирует содержимое. Попытка использовать string1 после этого вызовет ошибку компиляции. Такая модель обеспечивает предсказуемое управление ресурсами и исключает возможность доступа к невалидным данным.
Для работы с типами данных, которые можно легко скопировать, Rust предоставляет трейт Copy. Типы, реализующие эту трейт, копируются по значению при присваивании, и оригинальная переменная остается валидной. К таким типам относятся целочисленные типы, булевы значения и некоторые другие примитивы. Если тип не реализует трейт Copy, необходимо явно вызвать метод clone для создания копии данных.
let x = 5;
let y = x; // Копирование значения, x остается валидным
println!("x = {}, y = {}", x, y);
Разбор:
- Тип
i32(как и другие примитивы) реализуетCopy, поэтому присваивание создает побитовую копию значения. - После
let y = xобе переменные независимы, и использованиеxостается валидным. println!подтверждает, что перемещение владения не произошло и обе переменные доступны одновременно.- Этот паттерн важен для понимания, почему поведение
i32иStringв Rust принципиально отличается.
В этом случае значение 5 копируется из x в y, и обе переменные содержат независимые данные. Изменение y не влияет на x. Механизм выбора между перемещением и копированием определяется типом данных и наличием реализации трейта Copy.
Мини-кейс из практики: у вас есть сервис, который читает конфиг из файла и передаёт его в несколько функций. Если передавать String по значению без необходимости, вы быстро упрётесь в перемещения и лишние clone(). Если передавать &str там, где не нужна модификация, код становится легче, быстрее и понятнее.
Заимствование и ссылки (Borrowing)
Заимствование позволяет использовать данные без передачи права владения ими. В Rust существуют два вида заимствования: неизменяемая ссылка и изменяемая ссылка. Неизменяемая ссылка дает право читать данные, но запрещает их изменение. Несколько неизменяемых ссылок могут существовать одновременно, что обеспечивает безопасный параллельный доступ к данным. Изменяемая ссылка дает право изменять данные, но требует эксклюзивного доступа.
Правила заимствования строго контролируются компилятором. В любой области видимости может существовать либо одна изменяемая ссылка, либо любое количество неизменяемых ссылок. Невозможно иметь одновременно изменяемую ссылку и хотя бы одну неизменяемую ссылку на одни и те же данные. Это правило предотвращает состояния гонки и нарушения целостности данных при многопоточном выполнении.
Неизменяемые ссылки обозначаются символом &. Они позволяют передавать большие объемы данных в функции без копирования, сохраняя эффективность. Функция, принимающая неизменяемую ссылку, может читать данные, но не может их модифицировать. Это гарантирует, что вызывающий код не увидит неожиданных изменений в своих данных.
fn calculate_length(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let len = calculate_length(&s);
println!("The length of '{}' is {}.", s, len);
}
Разбор:
- Параметр
s: &Stringозначает неизменяемое заимствование: функция читает данные, но не получает владение. s.len()возвращает длину строки без копирования, потому что работает через ссылку.- В
mainвызовcalculate_length(&s)передает ссылку&s, поэтому после вызоваsостается доступной. - Сигнатура
-> usizeявно показывает, что функция возвращает числовой результат, а не изменяет исходную строку. - Это классический пример эффективной передачи больших данных без move и без clone.
В данном примере функция calculate_length принимает ссылку на строку и возвращает её длину. Строка s остается владеем функции main, а ссылка передается в функцию для чтения. После завершения вызова функция не оставляет побочных эффектов, и данные остаются доступными.
Изменяемые ссылки обозначаются символом &mut. Они используются, когда необходимо изменить данные внутри функции. Компилятор проверяет, что изменяемая ссылка является единственной активной ссылкой на объект в текущей области видимости. Это гарантирует, что изменения видны только через эту ссылку и не конфликтуют с другими операциями.
fn change(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
Разбор:
s: &mut Stringв параметре функции означает эксклюзивное заимствование с правом изменения данных.- Метод
push_strменяет исходный буфер строки на месте, не создавая новуюString. - В
mainпеременная должна быть объявлена какlet mut s, иначе мутабельная ссылка недоступна. - Вызов
change(&mut s)передает единственную активную mutable-ссылку, что соответствует правилам borrow checker. - После завершения функции ссылка освобождается, и
sснова можно использовать напрямую.
Здесь функция change получает изменяемую ссылку и добавляет текст к строке. Переменная s должна быть объявлена как mut, чтобы разрешить изменение через ссылку. Компилятор следит за тем, чтобы не было других активных ссылок на s во время вызова функции.
Долгая жизнь ссылок контролируется временем жизни (lifetime). Время жизни определяет период, в течение которого ссылка остается валидной. Компилятор анализирует код и назначает временные метки ссылкам, чтобы убедиться, что они не выходят за пределы области видимости данных, на которые они ссылаются. Это предотвращает появление висячих ссылок.
Типичная ошибка новичка — пытаться держать длинную &mut ссылку дольше, чем нужно. Рабочий приём: уменьшайте scope заимствования до минимального блока. Это сразу снимает множество конфликтов borrow checker.
Время жизни (Lifetime)
Время жизни в Rust описывает период, в течение которого ссылка считается действительной. Каждая ссылка имеет неявное или явное время жизни, которое связывает её с областью видимости источника данных. Компилятор использует систему временных меток для проверки соответствия времени жизни ссылки и времени жизни данных.
Когда функция принимает ссылки, компилятор анализирует зависимости между входными и выходными данными. Если функция возвращает ссылку, она должна указывать на данные, которые существуют дольше, чем сама ссылка. Компилятор проверяет это условие и выдает ошибку, если обнаруживает потенциальную проблему.
Анонимные времена жизни вводятся компилятором автоматически. Они обозначаются символами 'a, 'b, 'c и т.д., где 'a — самое короткое время жизни среди входных ссылок. Если функция принимает несколько ссылок, компилятор сопоставляет их с параметрами времени жизни и применяет ограничения к возвращаемому значению.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Разбор:
- Параметр времени жизни
<'a>связывает обе входные ссылки и возвращаемую ссылку в один контракт. - Сигнатура гарантирует: результат живет не дольше самого короткого из входных аргументов.
- Условие
if x.len() > y.len()выбирает один из аргументов без выделения новой памяти. - Возврат
&'a strисключает висячую ссылку на локальные временные значения. - Этот шаблон часто встречается в утилитах сравнения, где нужно вернуть ссылку на существующие данные.
В этой функции оба аргумента имеют одинаковое время жизни 'a. Возвращаемая ссылка также имеет время жизни 'a, что гарантирует, что результат будет валиден столько же, сколько и более короткий из входных аргументов. Компилятор проверяет, что ни один из аргументов не будет уничтожен раньше возврата значения.
Явные параметры времени жизни требуются, когда компилятор не может вывести их автоматически. Это случается в сложных случаях с несколькими ссылками и структурами данных. Параметр времени жизни указывается перед списком параметров функции и применяется ко всем ссылкам, которые зависят от него.
Структуры данных могут содержать ссылки, и тогда они также должны иметь параметр времени жизни. Это позволяет создавать структуры, которые хранят ссылки на внешние данные, оставаясь валидными пока эти данные существуют.
struct Review<'a> {
author: &'a str,
comment: &'a str,
}
Разбор:
struct Review<'a>хранит ссылки, поэтому параметр lifetime обязателен прямо в объявлении структуры.- Поля
authorиcommentс типом&'a strозначают, чтоReviewне владеет текстом, а заимствует его. - Один и тот же
'aуказывает, что обе ссылки подчиняются одинаковому ограничению времени жизни. - Такая модель экономит память и избегает лишних аллокаций, если исходные строки уже существуют где-то выше.
- При проектировании API это помогает разделить владеющие и невладеющие структуры явно.
Структура Review хранит две ссылки на строки. Параметр 'a связывает время жизни обеих ссылок. Объект Review не может жить дольше, чем минимальное время жизни его компонентов. Это обеспечивает безопасность при работе со ссылками внутри структур.
Слайсы (Slices)
Слайс представляет собой обзор части последовательности данных, такой как массив или строка. Он не владеет данными, а лишь указывает на начало и длину сегмента. Слайсы позволяют работать с подмножествами коллекций без копирования элементов, что экономит память и повышает производительность.
Тип слайса записывается как &[T], где T — тип элементов. Слайс содержит указатель на первый элемент и длину коллекции. Доступ к элементам осуществляется через индексацию, аналогичную массивам. Компилятор проверяет границы доступа и выдает ошибку, если индекс выходит за пределы слайса.
let v = vec![1, 2, 3, 4, 5];
let slice = &v[1..4];
// slice содержит элементы [2, 3, 4]
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. - Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. Vec— динамический массив в куче; удобен для push/pop и итераций.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
В этом примере создается слайс из вектора v, включающий элементы с индексами 1, 2 и 3. Слайс не копирует данные, а просто указывает на диапазон в памяти. Изменения через слайс отражаются в исходном векторе, если слайс изменяемый.
Строки в Rust являются последовательностями байтов UTF-8. Слайс строки работает по байтам, поэтому разрезание строки может привести к некорректному результату, если разрыв произойдет внутри многобайтового символа. Для безопасной работы со строками рекомендуется использовать методы, которые учитывают кодировку.
let s = String::from("привет мир");
let first_word = &s[..6]; // Может содержать частичный символ
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Для получения слова из строки лучше использовать методы, такие как split_whitespace или поиск пробела. Это гарантирует, что слайс будет начинаться и заканчиваться на границах символов.
Безопасное извлечение первого слова по границам UTF-8:
let s = "привет мир";
let first_word: &str = s.split_whitespace().next().unwrap_or("");
// first_word == "привет"
Разбор:
split_whitespace()режет строку по пробелам и возвращает итератор срезов&str, не копируя текст в новуюString..next()берёт первый элемент итератора; если слов нет, вернётсяNone.unwrap_or("")подставляет пустую строку вместо паники, когда вход пустой.- Тип результата
&strзаимствует данные изs, поэтомуsдолжен жить не короче, чем используетсяfirst_word.
Слайсы строк часто используются в функциях, которые принимают текстовые данные. Функция может принимать слайс &str, который является ссылкой на строковый слайс. Это позволяет передавать как строчные литералы, так и слайсы из объектов String.
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let s = String::from("Timur");
greet(&s);
greet("Guest");
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. fn main— точка входа: с неё начинается выполнение бинарника.- Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. - Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция greet принимает слайс строки. Вызов может передать ссылку на объект String или строчный литерал. Оба варианта работают корректно благодаря совместимости типов.
Обработка ошибок
В отличие от языков с исключениями (Java, C#, Python), в Rust ошибки выражаются типами — прежде всего Result<T, E> и Option<T>. Компилятор требует явно обработать Ok/Err (или пробросить через ?), что делает пути сбоя видимыми в сигнатуре функции. Panic — отдельный механизм для невосстановимых ситуаций, не замена try/catch.
Тип Result<T, E> имеет два варианта: Ok(T) для успешного результата и Err(E) для ошибки. Тип T представляет успешное значение, а E — тип ошибки. Компилятор требует обработки обоих вариантов, иначе код не скомпилируется.
Для "значения может не быть" используют Option<T>:
Код ITЗагрузка примера кода…
Разбор:
Option<u16>означает: либо есть число (Some), либо значения нет (None).matchобрабатывает оба варианта явно, как и дляResult.- Ветка
_ => Noneвозвращает "не найдено" для любого неизвестного имени. - Такой шаблон удобен для lookup-таблиц, кэшей и парсинга, где отсутствие результата — нормальный сценарий.
use std::fs::File;
fn main() {
let file_result = File::open("hello.txt");
let file = match file_result {
Ok(f) => f,
Err(error) => panic!("Проблема открытия файла: {:?}", error),
};
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. fn main— точка входа: с неё начинается выполнение бинарника.useподключает модули и типы, чтобы в теле кода использовать короткие имена.matchразбирает значение по вариантам; компилятор требует покрыть все допустимые случаи.Resultи оператор?делают ошибки частью API: неуспех возвращается наверх, а не теряется.- Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Операции с файлами возвращают
io::Result, поэтому ошибки диска и прав доступа нужно обрабатывать явно. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Конструкция match позволяет разобрать Result и выполнить логику для каждого случая. Если файл открылся успешно, переменная file содержит объект файла. Если возникла ошибка, код перехватывает её и выводит сообщение.
Метод unwrap() извлекает значение из Ok, но вызывает панику при ошибке. Метод expect() работает аналогично, но позволяет добавить собственное сообщение об ошибке. Эти методы удобны для прототипирования, но в продакшене рекомендуется использовать явную обработку.
let file = file_result.expect("Файл не найден");
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Метод map преобразует успешное значение, оставляя ошибку без изменений. Метод and_then позволяет цепочку операций, где следующая функция выполняется только при успехе предыдущей. Это упрощает работу с последовательными операциями ввода-вывода.
let result = file_result
.map(|f| f.read_to_string(&mut String::new()))
.and_then(|_| Ok("Успех"));
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. &mut— эксклюзивное заимствование: пока оно активно, другие ссылки на те же данные недопустимы.- Операции с файлами возвращают
io::Result, поэтому ошибки диска и прав доступа нужно обрабатывать явно. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Альтернативой match является оператор ?, который передает ошибку дальше, если она возникает. Оператор сокращает код и делает поток обработки более читаемым. Он работает только в функциях, возвращающих Result.
fn read_file() -> Result<String, std::io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.&mut— эксклюзивное заимствование: пока оно активно, другие ссылки на те же данные недопустимы.Resultи оператор?делают ошибки частью API: неуспех возвращается наверх, а не теряется.- Операции с файлами возвращают
io::Result, поэтому ошибки диска и прав доступа нужно обрабатывать явно. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
В этой функции каждый шаг с возможным завершением помечается знаком вопроса. Если любая операция вернет ошибку, функция немедленно возвращается с этим результатом. Успешное выполнение приводит к возврату строки.
Практическое правило: panic! оставляйте для невосстановимых состояний, а для ожидаемых ошибок используйте Result. В серверных и CLI-приложениях это напрямую влияет на стабильность и понятность диагностики.
Коллекции данных
Rust предоставляет встроенные коллекции данных для хранения множеств элементов. Вектор (Vec) представляет динамический массив, который может расти и уменьшаться в размерах. Он хранит элементы одного типа и управляет памятью автоматически. Элементы вектора располагаются в непрерывном участке памяти, что обеспечивает быстрый доступ по индексу.
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.Vec— динамический массив в куче; удобен для push/pop и итераций.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Вектор можно создать пустым и добавлять элементы методом push. Размер вектора увеличивается автоматически по мере необходимости. Компилятор выделяет память в куче и отслеживает её использование. При выходе из области видимости вектор освобождается автоматически.
Ссылки на элементы вектора получают через индексацию или итераторы. Индексация возвращает ссылку на элемент, если индекс валиден. Итераторы позволяют проходить по элементам без прямого доступа к памяти.
let v = vec![1, 2, 3, 4, 5];
for item in &v {
println!("{}", item);
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. - Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. Vec— динамический массив в куче; удобен для push/pop и итераций.- Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Цикл проходит по ссылке на каждый элемент вектора. Это безопасно и эффективно, так как не происходит копирования данных. Изменение элементов возможно через mutable итераторы.
Стек (Stack) реализуется через структуру данных LIFO. В Rust стек часто моделируется с помощью вектора, где добавление и удаление происходят с конца. Это обеспечивает высокую производительность операций push и pop.
let mut stack = Vec::new();
stack.push(1);
let top = stack.pop();
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.Vec— динамический массив в куче; удобен для push/pop и итераций.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Метод pop удаляет последний элемент и возвращает его. Если стек пуст, метод возвращает None. Это позволяет безопасно работать с пустыми структурами.
Хэш-карта (HashMap) хранит пары ключ-значение и обеспечивает быстрый поиск по ключу. Она использует хэш-функцию для распределения элементов по ячейкам. Хэш-карта подходит для случаев, когда порядок элементов не важен, а требуется быстрый доступ.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Team A", 10);
scores.insert("Team B", 20);
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. useподключает модули и типы, чтобы в теле кода использовать короткие имена.let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.HashMapдаёт быстрый доступ по ключу; порядок элементов не гарантирован.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Метод insert добавляет пару в карту. Метод get возвращает значение по ключу. Если ключа нет, возвращается None. Хэш-карта автоматически обрабатывает коллизии хэш-функций.
Связанный список (LinkedList) хранит элементы в узлах, соединенных ссылками. Он эффективен для частых вставок и удалений в середине коллекции, но медленнее для случайного доступа. В Rust связанный список доступен через модуль std::collections.
use std::collections::LinkedList;
let mut list = LinkedList::new();
list.push_back(1);
list.push_front(2);
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. useподключает модули и типы, чтобы в теле кода использовать короткие имена.let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Метод push_back добавляет элемент в конец, а push_front — в начало. Списки позволяют гибко управлять структурой данных без смещения остальных элементов.
Для прикладных задач в большинстве случаев достаточно Vec, HashMap и HashSet. LinkedList нужен редко. Если сомневаетесь, начинайте с Vec и переходите на другой тип только при ясной причине по профилированию.
Работа с файлами и вводом-выводом
Ввод-вывод в Rust осуществляется через модуль std::io. Функции и методы в этом модуле работают с буферами, потоками и файлами. Все операции ввода-вывода возвращают Result, что требует явной обработки ошибок.
Чтение из файла начинается с открытия файла через File::open. Результат проверяется, и если открытие успешно, данные считываются в буфер. Метод read_to_string читает всё содержимое файла в строку.
use std::fs::File;
use std::io::{self, Read};
fn read_file_contents(filename: &str) -> io::Result<String> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. useподключает модули и типы, чтобы в теле кода использовать короткие имена.let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.&mut— эксклюзивное заимствование: пока оно активно, другие ссылки на те же данные недопустимы.Resultи оператор?делают ошибки частью API: неуспех возвращается наверх, а не теряется.- Операции с файлами возвращают
io::Result, поэтому ошибки диска и прав доступа нужно обрабатывать явно. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция открывает файл, создает пустую строку и читает данные в неё. Знак вопроса передает ошибки вверх по стеку вызовов. Успешный результат возвращает содержимое файла.
Запись в файл требует открытия файла в режиме записи или создания. Метод write_all записывает байты в файл. Если файл не существует, он создается. Если существует, содержимое перезаписывается.
use std::fs::File;
use std::io::Write;
fn write_to_file(filename: &str, content: &str) -> io::Result<()> {
let mut file = File::create(filename)?;
file.write_all(content.as_bytes())?;
Ok(())
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. useподключает модули и типы, чтобы в теле кода использовать короткие имена.let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.- Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. Resultи оператор?делают ошибки частью API: неуспех возвращается наверх, а не теряется.- Операции с файлами возвращают
io::Result, поэтому ошибки диска и прав доступа нужно обрабатывать явно. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция создает файл и записывает строку в виде байтов. Ошибки ввода-вывода обрабатываются через ?. Успешное завершение возвращает единицу.
Стандартный вывод и ввод доступны через std::io::stdout и std::io::stdin. Метод print и println выводят текст в консоль. Метод read_line считывает строку из пользовательского ввода.
use std::io;
fn main() {
println!("Введите ваше имя:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Ошибка чтения");
println!("Привет, {}!", input.trim());
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. fn main— точка входа: с неё начинается выполнение бинарника.useподключает модули и типы, чтобы в теле кода использовать короткие имена.let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.&mut— эксклюзивное заимствование: пока оно активно, другие ссылки на те же данные недопустимы.- Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Программа запрашивает имя пользователя, считывает строку и выводит приветствие. Метод trim удаляет пробелы и переводы строк из введенного текста.
Модули и организация кода
Модули в Rust позволяют разделять код на логические блоки и управлять видимостью элементов. Каждый файл может содержать модуль, а пакеты могут включать несколько модулей. Структура проекта соответствует структуре модулей.
Объявление модуля делается с помощью ключевого слова mod. Оно создает новое пространство имен, куда можно поместить функции, структуры и константы. Элементы модуля скрыты по умолчанию, чтобы обеспечить инкапсуляцию.
mod backend;
fn main() {
backend::process_data();
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. fn main— точка входа: с неё начинается выполнение бинарника.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция process_data находится в модуле backend и доступна через полное имя. Чтобы сделать элемент видимым извне, нужно добавить атрибут pub.
pub fn process_data() {
// Логика обработки
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Атрибут pub делает функцию доступной для внешних модулей. Без него функция видна только внутри текущего модуля.
Подмодулы создаются путем объявления mod внутри другого модуля. Они организуют код иерархически, позволяя группировать связанные функции. Путь к элементу включает все уровни иерархии.
mod network {
pub mod http;
pub mod tcp;
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Модуль network содержит подмодули http и tcp. Элементы внутри них доступны через network::http::function.
Импорты позволяют сократить путь к элементам. Ключевое слово use загружает определение в текущее пространство имен. Можно импортировать конкретные элементы или весь модуль.
use std::collections::HashMap;
use crate::backend::process_data;
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. useподключает модули и типы, чтобы в теле кода использовать короткие имена.HashMapдаёт быстрый доступ по ключу; порядок элементов не гарантирован.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Импорт HashMap позволяет использовать его без префикса. Импорт функции process_data делает её доступной напрямую.
Глобальные алиасы создают краткие имена для длинных путей. Конструкция use ... as ... позволяет задать псевдоним.
use std::collections::HashMap as Map;
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. useподключает модули и типы, чтобы в теле кода использовать короткие имена.HashMapдаёт быстрый доступ по ключу; порядок элементов не гарантирован.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Теперь Map заменяет HashMap в коде. Это упрощает чтение и уменьшает количество повторяющихся слов.
Обобщения и трейты (Traits)
Генерика позволяет писать код, работающий с различными типами данных без потери информации о типе. Функции и структуры могут быть параметризованы типами, которые указываются при использовании.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. let mutобъявляет изменяемую переменную; безmutкомпилятор запретит запись через ссылку или прямое присваивание.- Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция largest принимает слайс любого типа T, который реализует трейт PartialOrd. Она возвращает ссылку на максимальный элемент. Тип T заменяется на конкретный тип при вызове функции.
Трейты определяют общее поведение для разных типов. Они подобны интерфейсам в других языках. Трейт описывает набор методов, которые должен реализовать тип.
Код ITЗагрузка примера кода…
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. - Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. structописывает состав данных; поля фиксируют модель предметной области.implпривязывает методы и связанные функции к типу.traitзадаёт контракт поведения для обобщённого кода.- Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Трейт Summary требует наличия метода summarize. Структура NewsArticle реализует этот трейт и предоставляет конкретную логику.
Трейты могут иметь методы по умолчанию. Если тип не переопределяет метод, используется версия из трейта. Это позволяет расширять функциональность без изменения базового кода.
trait Summary {
fn summarize(&self) -> String {
"Read more...".to_string()
}
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. - Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. traitзадаёт контракт поведения для обобщённого кода.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Если NewsArticle не переопределяет summarize, будет использоваться дефолтная реализация. Это упрощает создание общих интерфейсов.
Ограничения трейтов позволяют требовать реализацию нескольких трейтов одновременно. Синтаксис T: Trait1 + Trait2 указывает, что тип должен поддерживать оба трейта.
fn notify<T: Summary + Display>(item: &T) {
println!("{}", item.summarize());
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Отдельные функции (
fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода. - Неизменяемая ссылка
&Tпозволяет читать данные без передачи владения и без копирования. - Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция notify принимает любой тип, который реализует Summary и Display. Это обеспечивает гибкость и безопасность типов.
Макросы и метапрограммирование
Макросы позволяют генерировать код на этапе компиляции. Они расширяют синтаксис языка и позволяют создавать абстракции высокого уровня. Макросы работают с токенами и заменяют их на готовый код.
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
say_hello!();
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Макрос say_hello не принимает аргументов и выводит сообщение. Вызов макроса заменяется на тело макроса перед компиляцией.
Макросы с аргументами позволяют создавать более гибкие конструкции. Шаблон макроса описывает структуру аргументов и то, как они будут использованы.
macro_rules! print_twice {
($val:expr) => {{
println!("{}", $val);
println!("{}", $val);
}};
}
print_twice!(42);
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Аргумент $val подставляется в тело макроса. Блок кода оборачивается в фигурные скобки для создания новой области видимости.
Процедурные макросы представляют собой функции, которые получают AST (абстрактное дерево синтаксиса) и возвращают новый код. Они используются для создания атрибутов и функций, работающих с кодом.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. - Атрибут
#[derive(...)]генерирует реализации трейтов на этапе компиляции. structописывает состав данных; поля фиксируют модель предметной области.- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Атрибут derive вызывает процедурный макрос, который генерирует реализацию трейта Debug для структуры. Это избавляет от ручной реализации.
Макросы полезны для создания DSL (предметно-ориентированных языков) внутри Rust. Они позволяют писать код, который выглядит как декларативный язык, но компилируется в эффективный машинный код.
Асинхронность и задачи
Асинхронное программирование в Rust реализовано через библиотеку Tokio и стандартные средства. Модель асинхронности основана на задачах, которые выполняются конкурентно.
Токены async и await позволяют писать код, который может приостанавливаться и возобновляться. Функция, помеченная async, возвращает будущий результат, а не выполняет задачу сразу.
async fn fetch_data() -> String {
"data".to_string()
}
#[tokio::main]
async fn main() {
let data = fetch_data().await;
println!("{}", data);
}
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. fn main— точка входа: с неё начинается выполнение бинарника.async/.awaitпереводят ожидание I/O в неблокирующий режим под управлением рантайма (обычно Tokio).- Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция fetch_data асинхронна и возвращает строку. Вызов await приостанавливает выполнение до готовности результата. Токен #[tokio::main] запускает асинхронный цикл событий.
Задачи управляются планировщиком, который распределяет ресурсы между ними. Планировщик переключает контекст между задачами, обеспечивая параллельное выполнение.
Код ITЗагрузка примера кода…
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. fn main— точка входа: с неё начинается выполнение бинарника.useподключает модули и типы, чтобы в теле кода использовать короткие имена.async/.awaitпереводят ожидание I/O в неблокирующий режим под управлением рантайма (обычно Tokio).- Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Функция spawn создает задачу и возвращает дескриптор. Дескрипторы позволяют ждать завершения задач позже. Это обеспечивает гибкое управление потоками выполнения.
Асинхронные каналы позволяют передавать данные между задачами без блокировки. Каналы обеспечивают безопасность потоков и синхронизацию.
Код ITЗагрузка примера кода…
Разбор:
- Фрагмент показывает рабочий шаблон: его можно скопировать в
src/main.rsили модуль и сразу проверить черезcargo run. fn main— точка входа: с неё начинается выполнение бинарника.useподключает модули и типы, чтобы в теле кода использовать короткие имена.async/.awaitпереводят ожидание I/O в неблокирующий режим под управлением рантайма (обычно Tokio).- Макросы (с
!) разворачиваются при компиляции и убирают шаблонный код. - Макросы вывода помогают быстро проверить промежуточные значения при отладке.
- Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.
Канал mpsc поддерживает множественных отправителей. Отправка и получение блокируют задачу до готовности. Это упрощает координацию между задачами.
Частые ошибки новичков и быстрые решения
| Ситуация | Почему происходит | Что делать |
|---|---|---|
| "value borrowed here after move" | значение перемещено | передавать &T или &mut T, где не нужно владение |
много clone() в коде | попытка "побороть" borrow checker | пересобрать сигнатуры функций, сократить время жизни ссылок |
unwrap() в продакшен-коде | упрощение на старте | заменить на ?, match, map_err, доменные ошибки |
| блокирующий I/O в async | смешение sync и async API | использовать tokio::fs, tokio::net, spawn_blocking по необходимости |
Куда идти дальше по энциклопедии
- Базовый синтаксис ветвления и циклов: управляющие конструкции
- Сильная сторона языка в прикладном коде: важные трейты и типы
- Практика с данными и БД: работа с данными и структурами
- Первый рабочий проект: первая программа на Rust