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

Основы языка Rust

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

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


Основы языка Rust

Что такое Rust?

Rust — это язык программирования со следующими особенностями:

  • Типизация — статическая, сильная; вывод типов есть (let x = 42i32); явные аннотации при необходимости (x: f64).
  • Парадигма — мультипарадигменный — императивный, функциональный (итераторы, замыкания, match), процедурный; не классический ООП (нет наследования классов), вместо него — трейты и структуры.
  • Уровень — системный, ближе к низкоуровневому (доступ к железу, no_std, FFI), но с высокоуровневыми абстракциями без runtime-накладных расходов (zero-cost abstractions).
  • Выполнение — компилируемый AOT (rustc + LLVM); не интерпретируемый (есть miri для проверки unsafe, но не полноценный runtime).
  • Память — RAII через владение (ownership) и заимствование (borrowing); без сборщика мусора; детерминированное освобождение через drop.
  • Платформа — кроссплатформенный (Linux, Windows, macOS, embedded, WebAssembly); нативный машинный код; не управляемый runtime; компиляция в wasm32 для браузера и WASI.
  • Формат разработки — требует структуры проекта (Cargo, crate, src/main.rs); одиночный файл можно собрать через rustc, но идиоматичный цикл — cargo new / cargo run.
  • Направление — системное программирование (ядра, драйверы, embedded), инфраструктура (сети, БД, прокси), CLI, критичные компоненты, WebAssembly; не универсальный "язык для всего".
  • REPL — встроенного REPL нет; для экспериментов — Rust Playground или сторонний evcxr; основной цикл — cargo run и компиляция.
  • Поколение — современный (с 2010, стабильный 1.0 в 2015); editions 2015, 2018, 2021, 2024 с обратной совместимостью.
  • Параллелизм и асинхронность — нативные потоки ОС (std::thread); async/await (Tokio, async-std); borrow checker исключает data races в safe Rust на этапе компиляции.
  • Безопасность — memory-safe в safe Rust (проверки на этапе компиляции); unsafe для низкоуровневых операций и FFI; panic вместо исключений; логические ошибки остаются на совести разработчика.

Если какой-то пункт из списка непонятен — подробные определения и примеры в Язык программирования.

Картина целиком после первой программы — зачем Rust, fn main, макрос println!, почему компилятор строг к памяти. Подробно про владение — в типах.

Маршрут: эта статья → синтаксисвладениеошибки.


Rust как ответ на системные вызовы современной инженерии

Rust — язык системного программирования (с 2006 — Mozilla Research, с 2021 — Rust Foundation). Идея — производительность, безопасность и удобство без GC — за счёт владения и заимствования, которые проверяет компилятор.

Ниже — "Hello, world" с разбором строк (тот же код, что в первой программе).

fn main() {
println!("Hello, world!");
}

Разбор:

  • fn main — точка входа исполняемого Rust-приложения, с нее начинается выполнение программы.
  • println! — макрос вывода в стандартный поток, поэтому он работает без дополнительной настройки.
  • Литерал "Hello, world!" попадает в бинарник напрямую и выводится в консоль в момент вызова.
  • Этот пример используют как минимальную проверку: компилятор, запуск и базовый синтаксис работают корректно.

fn - ключевое слово, которое обозначает начало объявления функции. Функция представляет собой именованный блок кода, выполняющий конкретную задачу. В данном случае функция называется main.

main - имя специальной функции, которая запускается автоматически при запуске программы. Система операционная система вызывает именно эту функцию для начала выполнения логики приложения. Любая исполняемая программа на Rust должна содержать функцию с именем main.

Фигурные скобки определяют область видимости тела функции. Весь код, находящийся внутри этих скобок, выполняется последовательно по порядку следования строк.

println! - это макрос (макрос — это инструмент, расширяющийся во время компиляции), который выводит текст на стандартный поток вывода, обычно это терминал или консоль. Знак восклицания после имени указывает на то, что это макрос, а не обычная функция. Макрос обрабатывает форматирование строки перед передачей данных в систему вывода. Строка заключена в двойные кавычки, что сообщает компилятору о том, что содержимое следует воспринимать как последовательность символов текста.

Язык не является надмножеством C, не пытается эмулировать C++ или Go. Он синтезирует успешные идеи из нескольких областей:

  • низкоуровневый контроль (как в C/C++),
  • строгая статическая проверка (как в Haskell или ML),
  • современные инструменты сборки и управления зависимостями (как в Node.js или Python),
  • языковая поддержка конкурентности без гонок данных (впервые реализованная на уровне компилятора в промышленном масштабе).

Rust не позиционирует себя как универсальный "язык для всего", но как язык для критичных к надёжности и производительности компонентов, которые ранее требовали ручного управления памятью, но не должны нести на себе бремя частых уязвимостей и непредсказуемого поведения. Это отражено в его слогане: "A language empowering everyone to build reliable and efficient software".


Почему Rust был необходим

До появления Rust системное программирование разделялось на две модели:

  1. Языки без автоматического управления памятью — C и C++. Высокая производительность, полный контроль над памятью и железом, но высокая цена — уязвимости типа use-after-free, double-free, buffer overflows и data races составляют значительную долю CVE в ядрах ОС, сетевых стеках и криптографических библиотеках.

  2. Языки с автоматическим управлением памятью (сборщиком мусора) — Java, C#, Go. Повышается безопасность и продуктивность, однако:

    • GC вводит непредсказуемые паузы и накладные расходы по памяти;
    • неприемлем для систем без heap-менеджера или с жёсткими требованиями к latency (real-time systems, embedded, kernels);
    • FFI-взаимодействие с нативным кодом требует осторожности и часто отменяет преимущества GC.

Между этими полюсами образовался вакуум — не существовало языка, который бы обеспечивал нулевые накладные расходы (zero-cost abstractions), гарантированную безопасность памяти на этапе компиляции и полный контроль над моделью исполнения. Rust был создан как эксперимент по заполнению этой ниши.

Первый стабильный релиз (1.0) состоялся в 2015 году. С тех пор Rust демонстрирует устойчивый рост в индустриальных применениях: от ядра Linux (начиная с 6.1), через Android (частичная замена C/C++ в системных компонентах), до Microsoft (переписывание компонентов Windows на Rust) и Amazon (использование в AWS Nitro, Firecracker и S2N).


Ключевые особенности языка

Безопасность памяти без сборщика мусора

В Rust отсутствует runtime-сборка мусора. Вместо этого безопасность памяти обеспечивается статически, на этапе компиляции, с помощью трёх взаимосвязанных механизмов:

  • Владение (Ownership) — каждое значение в Rust имеет ровно одного владельца; при выходе владельца из области видимости значение автоматически освобождается.
  • Заимствование (Borrowing) — ссылки на значение могут быть иммутабельными (&T) или мутабельными (&mut T), но при соблюдении строгих правил:
    • В любой момент времени может существовать либо произвольное количество иммутабельных ссылок, либо ровно одна мутабельная;
    • Ссылки не могут "пережить" данные, на которые они ссылаются.
  • Время жизни (Lifetimes) — механизм, позволяющий компилятору проверять, что ссылки остаются валидными на всём протяжении их использования. Явные аннотации ('a) требуются только в сигнатурах функций и структур, где вывод недостаточен.

Разберём примеры.

Пример 1: Владение и автоматическое освобождение памяти. Переменная s становится владельцем строки данных. Когда переменная выходит за пределы области видимости (закрывающая фигурная скобка), компилятор автоматически вызывает функцию удаления, освобождая память.

fn main() {
// Строка "Hello" создаётся в куче, s становится её владельцем
let s = String::from("Привет, мир!");

println!("Владелец s: {}", s);

// Выход из области видимости функции main
// Компилятор автоматически генерирует вызов drop(s) здесь
}

Разбор:

  • String::from(...) выделяет память в куче, а переменная s становится владельцем этой памяти.
  • Пока s живет в области видимости, код может безопасно использовать строку без риска use-after-free.
  • После выхода из main вызывается drop, и освобождение памяти происходит автоматически и детерминированно.
  • Фрагмент иллюстрирует главный принцип ownership: ресурс освобождается вместе с владельцем.

При выполнении программы строка "Привет, мир!" остаётся в памяти до момента завершения функции main. Система гарантирует отсутствие утечек памяти, так как удаление происходит синхронно с выходом владельца из зоны действия.

Пример 2: Заимствование и правила доступа. Несколько ссылок на одно значение могут существовать одновременно, но ни одна из них не позволяет изменять данные. Попытка создать мутабельную ссылку при наличии иммутабельной вызовет ошибку компиляции.

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

Разбор:

  • &data создает неизменяемые ссылки для чтения, поэтому в этом месте можно только читать строку.
  • Комментарий с &mut data показывает запрещенный сценарий: mutable-ссылка не может сосуществовать с immutable-ссылками.
  • После завершения внутреннего блока ссылки ref1 и ref2 перестают жить, и mutable-заимствование становится валидным.
  • Разыменование *ref_mut = ... явно показывает, где происходит изменение данных через ссылку.
  • Этот пример фиксирует базовое правило borrow checker про эксклюзивность записи.

Компилятор отслеживает, где ещё "живы" заимствования (в том числе с учётом non-lexical lifetimes — до последнего использования, а не только до }). Он запрещает одновременное наличие одной мутабельной и любого количества иммутабельных ссылок на одни и те же данные, что предотвращает data races и повреждение памяти в safe-коде.

Пример 3: Время жизни и проверка валидности. Этот код иллюстрирует, как механизм времени жизни защищает от использования ссылок после уничтожения данных, на которые они указывают. Попытка вернуть ссылку на локальную переменную приведёт к ошибке, так как ссылка переживёт свои данные.

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

Разбор:

  • В invalid_reference локальная переменная x уничтожается при выходе из функции, поэтому ссылка на нее не может быть возвращена.
  • Ошибка lifetime возникает на этапе компиляции и предотвращает висячую ссылку еще до запуска программы.
  • valid_ownership() -> String показывает корректный паттерн: вернуть владение значением, а не ссылку на временный объект.
  • Контраст двух функций помогает понять, что lifetime-ограничения являются частью интерфейса функции.
  • Этот подход напрямую снижает риск memory-багов в прикладном коде.

Механизм времени жизни анализирует структуру кода статически: ссылка не должна пережить данные, на которые она указывает. В safe Rust успешная компиляция означает отсутствие use-after-free, double-free и data races в смысле модели языка; это не отменяет паник, логических ошибок и ответственности за код в блоках unsafe и на границе FFI.

Пример 4: Мутабельность и конфликты ссылок. Демонстрация строгого запрета на одновременное изменение и чтение одного и того же блока данных.

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

Разбор:

  • Фрагмент показывает рабочий шаблон: его можно скопировать в src/main.rs или модуль и сразу проверить через cargo run.
  • fn main — точка входа: с неё начинается выполнение бинарника.
  • use подключает модули и типы, чтобы в теле кода использовать короткие имена.
  • let mut объявляет изменяемую переменную; без mut компилятор запретит запись через ссылку или прямое присваивание.
  • &mut — эксклюзивное заимствование: пока оно активно, другие ссылки на те же данные недопустимы.
  • Result и оператор ? делают ошибки частью API: неуспех возвращается наверх, а не теряется.
  • Макросы (с !) разворачиваются при компиляции и убирают шаблонный код.
  • Vec — динамический массив в куче; удобен для push/pop и итераций.

В safe Rust borrow checker исключает data races и ряд ошибок памяти на этапе компиляции. Это не отменяет Mutex, RwLock и атомики, когда нужен разделяемый изменяемый доступ (Arc<Mutex<T>> и т.д.). Проверки владения не заменяют синхронизацию произвольного shared state.

Эта система исключает классические ошибки:

  • Use-after-free — невозможно создать ссылку, срок жизни которой превышает срок жизни данных;
  • Double-free — значение освобождается ровно один раз, когда выходит из области видимости его единственный владелец;
  • data races — при компиляции в многопоточном коде: невозможна одновременная запись и чтение/запись одной переменной без синхронизации.

Модель опирается на аффинные/линейные типы и правила владения; формальные результаты (RustBelt и др.) касаются подмножества семантики. Компилятор проверяет правила владения и заимствований в safe-коде; не гарантирует отсутствие логических багов, panic, ошибок в unsafe и за границей FFI.


Zero-cost abstractions

Rust следует принципу zero-cost abstractions — любая высокоуровневая конструкция (итераторы, замыкания, паттерн-матчинг, монадоподобные типы Option/Result) после компиляции в оптимизированный режим (--release) генерирует код, идентичный или близкий к написанному вручную на C.

Например:

  • for x in vec.iter() → компилируется в bare-pointer loop без вызовов функций;
  • map().filter().collect() → инлайнится в один цикл;
  • Result<T, E> не накладывает оверхеда по сравнению с возвратом кода ошибки в регистре.

Это позволяет писать выразительный, функционально-вдохновлённый код, не платя за него в runtime.

Итераторы в действии:

fn main() {
let nums = vec![1, 2, 3, 4, 5];
let sum: i32 = nums.iter().filter(|n| *n % 2 == 0).sum();
println!("сумма чётных: {sum}");
}

Разбор:

  • nums.iter() создаёт итератор по ссылкам на элементы, без копирования всего вектора.
  • .filter(|n| *n % 2 == 0) оставляет только чётные значения; *n разыменовывает ссылку &i32.
  • .sum() агрегирует оставшиеся элементы в одно число типа i32.
  • В release-сборке такая цепочка часто сворачивается компилятором в один эффективный цикл.

Выразительный и строгий типизированный язык

Rust — язык со статической, строгой, выводимой типизацией.

Типовая система включает:

  • Параметрический полиморфизм (generics);
  • Ад-хок полиморфизм через трейты (аналог интерфейсов, но с мощными расширениями — ассоциированные типы, дефолтные реализации, ограничения на lifetime);
  • Суммарные и произведение типов (enum и struct), включая алгебраические типы данных (ADT), например:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}

Разбор:

  • Фрагмент показывает рабочий шаблон: его можно скопировать в src/main.rs или модуль и сразу проверить через cargo run.

  • enum объединяет взаимоисключающие состояния в один тип.

  • Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.

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

Особое место занимает обработка ошибок: вместо исключений (exceptions) Rust использует явную обработку через Result<T, E> и Option<T>. Это делает поток ошибок видимым на уровне типов и исключает "скрытые" пути выполнения.


Инкрементальная и надёжная компиляция

Rust использует компилятор rustc, основанный на LLVM, и систему сборки Cargo. Cargo обеспечивает:

  • Управление зависимостями с семантическим версионированием и изоляцией (crates);
  • Репродюцируемую сборку (с фиксированным Cargo.lock);
  • Встроенные средства тестирования, документации (cargo doc), проверки (cargo clippy, cargo fmt);
  • Поддержку кросс-компиляции "из коробки".

Модель crate (единица компиляции) и строгая система modules позволяют строить крупные проекты с чёткими границами видимости и инкапсуляцией — без необходимости в отдельных файлах заголовков или make-файлах.


Сфера применения

Применения, где Rust проявляет себя наилучшим образом

  • Системное программирование — ядра ОС (Redox OS, компоненты Linux), драйверы устройств, firmware (через no_std), микроконтроллеры (STM32, ESP32). Возможность отключения стандартной библиотеки (#![no_std]) позволяет работать в средах без heap и OS.

  • Инфраструктурные компоненты — сетевые серверы (Tokio, Actix), прокси (Linkerd, Envoy-подобные), базы данных (TiKV, SurrealDB), веб-асемблер (Wasmtime, WASI), криптографические библиотеки (ring, Rustls). Здесь важны предсказуемость latency и отсутствие GC-пауз.

  • Компиляторы, анализаторы, транспайлеры — благодаря мощной системе макросов (declarative и procedural), строгой типизации и инструментам вроде syn, quote, proc-macro2. Примеры — rustc сам, clippy, serde, tauri.

  • Кросс-платформенные CLI-утилиты: благодаря статической линковке (возможна), единому экзешнику и отсутствию зависимостей runtime.

  • Блокчейны и децентрализованные системы — Ethereum (Parity, Substrate), Solana, NEAR — где критична детерминированность выполнения и защита от уязвимостей.

  • Встраивание в другие языки — через FFI Rust может выступать "усилителем" для Python (PyO3), JavaScript (wasm-bindgen, Neon), Ruby (Rutie), Java (JNI). Часто используется для написания performance-critical hot paths.


Границы применимости

Rust не идеален для:

  • Быстрой прототипной разработки под задачи аналитики, ML или визуализации — здесь Python/Julia/JS остаются эффективнее по времени разработки.
  • GUI-приложений с плотной интеграцией в нативные фреймворки — хотя решения есть (egui, Slint, Tauri, Iced), экосистема уступает в зрелости C#/WPF или Swift/UIKit.
  • Веб-фронтенда без WebAssembly — JS/TS остаются стандартом; Rust здесь — инструмент для ускорения конкретных модулей.
  • Образовательных целей "с нуля" — крутая кривая обучения из-за модели владения затрудняет первые шаги; проще начинать с Python или JS.

Однако даже в этих областях Rust находит применение как компоновочный язык — например, Tauri использует Rust для бэкенда, а HTML/JS — для фронтенда; PyO3 — для ускорения compute-heavy частей в Python-библиотеках.


Экосистема и культура

Rust не ограничивается синтаксисом. Это языковая экосистема, включающая:

  • RFC-процесс — все изменения языка проходят открытую дискуссию и формальную проработку;
  • Editions — мажорные релизы каждые 3 года (2015, 2018, 2021, 2024), сохраняющие обратную совместимость, но позволяющие эволюционировать без "языкового раскола";
  • Clippy и rustfmt — встроенные инструменты, обеспечивающие единый стиль и качество кода по умолчанию;
  • Crates.io — централизованный реестр пакетов с жёсткими ограничениями на именование и версионирование.

Культура сообщества делает ставку на ясность, надёжность и инклюзивность. Документация (The Rust Book, Rust By Example, nomicon) считается одной из лучших в индустрии. Компилятор выдаёт обучающие подсказки с примерами исправлений.


Синтаксис Rust

На первый взгляд, синтаксис Rust напоминает C/C++/Java — фигурные скобки, fn, let, if, loop, match, типы после именования переменной (x: i32). Однако это сходство поверхностно. Rust использует алгебраический и выражение-ориентированный синтаксис, в котором почти всё — выражение (expression), возвращающее значение. Это создаёт принципиально иную модель композиции кода.

Базовые объявления переменных и типов:

fn main() {
let answer = 42; // тип i32 выведен компилятором
let ratio: f64 = 0.5; // тип указан явно
let mut counter = 0; // изменяемая переменная
counter += 1;
println!("answer={answer}, ratio={ratio}, counter={counter}");
}

Разбор:

  • let создаёт неизменяемую по умолчанию привязку; без mut значение нельзя менять.
  • Аннотация : f64 фиксирует тип, когда вывод недостаточен или нужен конкретный тип.
  • mut разрешает изменение (+=, присваивание, &mut в функции).
  • Интерполяция {answer} в println! подставляет значения без конкатенации строк.

Выражения и операторы

В отличие от C/C++ (где if, loop, while, matchоператоры, не возвращающие значение), в Rust:

  • if, match, loopbreak value), блоки { … }выражения;
  • let, fn, use, struct, enum, traitобъявления (items);
  • присваивание (x = y), вызовы функций без возвращаемого значения (()) — операторы, но возвращают unit-type ().

Пример:

let x = if condition {
42
} else {
0
};
// x имеет тип i32, значение зависит от ветки if

Разбор:

  • Фрагмент показывает рабочий шаблон: его можно скопировать в src/main.rs или модуль и сразу проверить через cargo run.
  • Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.

Это не просто "удобство". Это гарантия отсутствия неинициализированных переменных — переменная x инициализируется всегда, и компилятор требует, чтобы все ветки if и match возвращали значение одного типа. Это исключает целый класс ошибок uninitialised reads.

Аналогично, match не просто замена switch — это исчерпывающая проверка вариантов:

enum Message {
Quit,
Write(String),
Move { x: i32, y: i32 },
}
let msg = Message::Write("hello".into());
match msg {
Message::Quit => println!("Quitting"),
Message::Write(text) => println!("Text: {}", text),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
// _ => { } — запрещено: все варианты должны быть обработаны явно
}

Разбор:

  • enum Message объединяет несколько различных форм данных в единый тип сообщений.
  • В match каждая ветка не только проверяет вариант, но и извлекает связанные данные (text, x, y).
  • Компилятор требует полное покрытие вариантов, поэтому добавление нового варианта сразу подсветит все места, где нужно обновить логику.
  • Такой стиль помогает делать безопасный рефакторинг: пропущенная ветка блокирует сборку, а не ломает поведение в runtime.
  • Это один из главных инструментов моделирования состояния в Rust-коде.

Компилятор проверяет полноту покрытия, и если добавить новый вариант в enum, все match-выражения, не покрывающие его, перестанут компилироваться. Это делает рефакторинг безопасным — не нужно искать все case в кодовой базе.


Отсутствие неявных приведений и скрытых эффектов

Rust решительно отказывается от:

  • неявного приведения между целыми типами (i32u64);
  • неявного преобразования 0false, ""false;
  • неявного копирования (deep/shallow copy) сложных типов;
  • неявных конструкторов/деструкторов (в стиле C++);
  • скрытых аллокаций (например, при конкатенации String в цикле).

Всё, что может повлечь за собой:

  • аллокацию памяти,

  • копирование данных,

  • изменение состояния внешней переменной, — должно быть выражено явно. Например:

  • s.push_str("x") — изменяет s, но не создаёт новую строку;

  • s + "x" — создаёт новую String, требует владения s;

  • &s + "x" — ошибка: нельзя сложить &str и &str без аллокации;

  • format!("{}x", s) — аллокация, но явная.

Это инструмент предсказуемости. Разработчик всегда видит, где происходит:

  • передача владения (move);
  • заимствование (&);
  • аллокация (String::from, vec!);
  • копирование (clone(), copy-типы).

Макросы — гигиеничные, типобезопасные, компиляционные

println!, vec!, format!, dbg!, #[derive(Debug)] — всё это макросы, но не C-препроцессорные текстовые подстановки. Rust использует процедуральные и декларативные макросы, работающие на AST-уровне (после парсинга, до типизации). Они:

  • гигиеничны: не захватывают переменные извне;
  • типобезопасны: не компилируются, если переданы аргументы неверного типа;
  • могут генерировать код, адаптивный к контексту (например, vec![1, 2, 3] создаёт Vec<i32>, а vec!["a", "b"]Vec<&str>).

Макросы — расширение языка, одобренное компилятором. Например, serde генерирует сериализаторы/десериализаторы во время компиляции, без рантайм-рефлексии — и генерируемый код типизирован и проверен.


Модель владения

Часто говорят: "Rust сложен из-за borrow checker". На деле, borrow checker — это реализация более глубокой идеи: линейных ресурсов.


Владение как контракт

Каждое значение имеет:

  • одного владельца (владение передаётся при присваивании и передаче в функцию);
  • нуль или более заимствований, но с двумя взаимоисключающими режимами:
    • shared borrow (&T) — можно читать, но не писать; сколько угодно одновременно;
    • exclusive borrow (&mut T) — можно читать и писать; строго одна в области видимости.

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

  • циклов (для &mut);
  • пересечений &mut и &;
  • ссылок, "выходящих" за пределы данных.

Результат — отсутствие гонок данных (data races) в безопасном (safe) Rust. Это формально подтверждено: в 2019 году Aaron Turon и др. доказали, что borrow checker исключает data races в рамках Rust memory model.


Время жизни

Аннотации 'a в &'a T не являются "опциональными подсказками". Они — часть типа. Тип &T — это сокращение для &'_ T, где '_выведенное время жизни. Но когда ссылки появляются в:

  • аргументах функции,
  • возвращаемых значениях,
  • полях структур, — время жизни должно быть указано явно или выведено однозначно.

Пример:

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

Разбор:

  • Фрагмент показывает рабочий шаблон: его можно скопировать в src/main.rs или модуль и сразу проверить через cargo run.
  • Отдельные функции (fn) выносят логику в именованные блоки с явной сигнатурой входа и выхода.
  • Параметр lifetime 'a связывает ссылки в сигнатуре и гарантирует, что результат не переживёт данные.
  • Для практики полезно изменить входные данные, спровоцировать ошибку и прочитать сообщение компилятора или runtime.

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

В отличие от C (где const char* longest(const char*, const char*) не гарантирует ничего о lifetime), Rust делает время жизни частью API.


Copy и Clone — явное разделение "лёгкого копирования" и "глубокого"

Типы в Rust делятся на:

  • Copy — копируются побитово при присваивании/передаче; не имеют деструктора (Drop); только стековые типы без указателей (например, i32, bool, (i32, f64));
  • !Copy — передаются по владению; копирование требует явного .clone().

Это дизайн-решение — разработчик выбирает, какие типы "дешёвые" (и могут копироваться неявно), а какие — "дорогие" (и требуют осознанного копирования). Например:

  • String — не Copy: владеет heap-буфером;
  • &str — не Copy, но Copy-подобен: &str — это fat pointer (адрес + длина), и он Copy;
  • Vec<T> — не Copy: владеет буфером памяти.

Таким образом, Rust не скрывает стоимость операций.


Безопасность

В Rust различают:

  • safe Rust — код без unsafe, прошедший проверки borrow checker’а, типизации и инициализации. Для такого кода компилятор исключает:
    • use-after-free,
    • double-free,
    • data races,
    • чтение неинициализированной памяти. При этом возможны panic (например, выход за границы при arr[i] или явный panic!), в debug — паника при переполнении целых, в release — часто обёртывание (wrapping) по правилам типа; логическая корректность остаётся на совести разработчика.
  • unsafe Rust — блоки unsafe { … }, где разрешены:
    • разыменование "сырых" указателей (*const T, *mut T);
    • вызов unsafe-функций (например, из FFI);
    • реализация unsafe trait;
    • мутация статических переменных.

unsafe не отключает borrow checker. Он лишь расширяет поверхность допустимого, но обязанность доказать безопасность ложится на программиста. При этом:

  • unsafe-блок должен быть минимальным;
  • он должен быть обёрнут в safe-интерфейс (например, Vec::push использует unsafe внутри, но представляет safe API);
  • экосистема придерживается принципа "unsafe in safe": чем меньше unsafe, тем выше доверие.

Это делает Rust практичным для системного кода — аллокаторы, атомики и FFI реализуются с unsafe внутри библиотек, а прикладной код обычно пишут в safe-подмножестве, не размножая unsafe по проекту.


Инструментарий

Rust не просто язык — это платформа разработки:

  • rustc — компилятор с детальными диагностиками (включая подсказки вида "попробуйте добавить mut здесь");
  • Cargo — система сборки, управления зависимостями, тестирования, документирования;
  • rustup — менеджер инструментария с поддержкой nightly/stable/beta, target-триплетов, компонентов (rust-src, rustfmt, clippy);
  • rustfmt — форматтер, обеспечивающий единый стиль по умолчанию;
  • clippy — линтер, выявляющий антипаттерны, неочевидные ошибки, неидиоматичный код;
  • miri — интерпретатор MIR для динамической проверки UB; особенно полезен для unsafe и тонких правил aliasing, не только для safe-кода.

Эти инструменты не "дополнения" — они встроены в workflow. Например:

cargo new project
cd project
cargo build # сборка
cargo run # запуск
cargo test # unit/integration-тесты
cargo doc --open # генерация и просмотр документации
cargo clippy # статический анализ
cargo fmt # форматирование

Разбор:

  • cargo new создает каркас проекта с исходниками и манифестом зависимостей.
  • cargo build и cargo run покрывают основной цикл разработки: собрать и запустить.
  • cargo test автоматизирует проверку корректности функциональности при изменениях.
  • cargo clippy помогает находить неочевидные ошибки и неидиоматичные конструкции.
  • cargo fmt синхронизирует стиль кода в команде и упрощает code review.

Это создаёт единый стандарт качества даже в распределённых командах.