Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Знакомство с курсом и общий подход

Что вы узнаете: структуру курса, интерактивный формат и то, как знакомые концепции C/C++ соответствуют своим аналогам в Rust. Эта глава задаёт ожидания и даёт дорожную карту для остальной книги.

  • О курсе
    • Курс основан на оригинальных материалах Rust Training (лицензии MIT и CC-BY-4.0)
    • Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus
  • Курс задуман как можно более интерактивный
    • Предположение: вы знаете C, C++ или оба языка
    • Примеры намеренно построены так, чтобы знакомые концепции сопоставлялись с аналогами в Rust
    • Задавайте уточняющие вопросы в любой момент
  • Преподаватель рассчитывает на продолжение общения с командами

Аргументы в пользу Rust

Хотите сразу перейти к коду? Перейдите к разделу Покажите мне код

Независимо от того, приходите ли вы из C или C++, основные болевые точки одни и те же: ошибки безопасности памяти, которые компилируются без проблем, но приводят к падению, порче данных или утечкам во время выполнения.

  • Более 70% CVE вызваны проблемами безопасности памяти — переполнениями буфера, висячими указателями, использованием после освобождения
  • Умные указатели C++ (shared_ptr, unique_ptr), RAII и семантика перемещения — шаги в правильном направлении, но это пластыри, а не лекарство: они оставляют открытыми использование после перемещения, циклы ссылок, инвалидацию итераторов и пробелы в безопасности исключений
  • Rust даёт ту же производительность, на которую вы опираетесь в C/C++, но с гарантиями безопасности на этапе компиляции

📖 Подробнее: см. Почему разработчикам C/C++ нужен Rust — там конкретные примеры уязвимостей, полный список того, что устраняет Rust, и объяснение, почему умных указателей C++ недостаточно


Как Rust решает эти проблемы?

Переполнения буфера и выход за границы

  • У всех массивов, срезов и строк Rust явно заданы границы. Компилятор вставляет проверки, благодаря которым любой выход за границы приводит к аварийному завершению во время выполнения (panic в терминологии Rust), а не к неопределённому поведению

Висячие указатели и ссылки

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

Использование после перемещения

  • Система владения в Rust делает перемещение разрушающим: после перемещения значения компилятор не позволит использовать исходную переменную. Никаких «зомби-объектов» и состояния «допустимо, но не определено»

Управление ресурсами

  • Трейт Drop в Rust — это RAII, реализованный правильно: компилятор автоматически освобождает ресурсы, когда они выходят из области видимости, и предотвращает использование после перемещения, чего RAII в C++ не может
  • Правило пяти не нужно (не требуется определять конструктор копирования, конструктор перемещения, оператор копирующего присваивания, оператор перемещающего присваивания и деструктор)

Обработка ошибок

  • В Rust нет исключений. Все ошибки — это значения (Result<T, E>), поэтому обработка ошибок явная и видна в сигнатуре типа

Инвалидация итераторов

  • Проверка заимствований в Rust запрещает изменять коллекцию во время итерации по ней. Вы просто не можете написать те ошибки, которыми полны кодовые базы на C++:
#![allow(unused)]
fn main() {
// Аналог удаления во время итерации в Rust: retain()
pending_faults.retain(|f| f.id != fault_to_remove.id);

// Или: собрать в новый Vec (функциональный стиль)
let remaining: Vec<_> = pending_faults
    .into_iter()
    .filter(|f| f.id != fault_to_remove.id)
    .collect();
}

Гонки данных

  • Система типов предотвращает гонки данных на этапе компиляции благодаря трейтам Send и Sync

Визуализация безопасности памяти

Владение в Rust — безопасность по замыслу

#![allow(unused)]
fn main() {
fn safe_rust_ownership() {
    // Перемещение разрушающее: исходное значение больше недоступно
    let data = vec![1, 2, 3];
    let data2 = data;           // Происходит перемещение
    // data.len();              // Ошибка компиляции: значение использовано после перемещения
    
    // Заимствование: безопасный совместный доступ
    let owned = String::from("Hello, World!");
    let slice: &str = &owned;  // Заимствование — без выделения памяти
    println!("{}", slice);     // Всегда безопасно
    
    // Висячие ссылки невозможны
    /*
    let dangling_ref;
    {
        let temp = String::from("temporary");
        dangling_ref = &temp;  // Ошибка компиляции: temp не живёт достаточно долго
    }
    */
}
}
graph TD
    A[Безопасность владения Rust] --> B[Разрушающие перемещения]
    A --> C[Автоматическое управление памятью]
    A --> D[Проверка времён жизни на этапе компиляции]
    A --> E[Никаких исключений — типы Result]
    
    B --> B1["Использование после перемещения — ошибка компиляции"]
    B --> B2["Никаких зомби-объектов"]
    
    C --> C1["Трейт Drop = RAII, сделанный правильно"]
    C --> C2["Правило пяти не нужно"]
    
    D --> D1["Проверка заимствований исключает висячие ссылки"]
    D --> D2["Ссылки всегда валидны"]
    
    E --> E1["Result<T,E> — ошибки в типах"]
    E --> E2["Оператор ? для распространения"]
    
    style A fill:#51cf66,color:#000
    style B fill:#91e5a3,color:#000
    style C fill:#91e5a3,color:#000
    style D fill:#91e5a3,color:#000
    style E fill:#91e5a3,color:#000

Структура памяти: ссылки в Rust

graph TD
    RM1[Стек] --> RP1["Ссылка &i32"]
    RM2[Стек/Куча] --> RV1["Значение i32 = 42"]
    RP1 -.->|"Безопасная ссылка — время жизни проверяется"| RV1
    RM3[Проверка заимствований] --> RC1["Исключает висячие ссылки на этапе компиляции"]
    
    style RC1 fill:#51cf66,color:#000
    style RP1 fill:#91e5a3,color:#000

Визуализация выделения памяти в Box<T>

#![allow(unused)]
fn main() {
fn box_allocation_example() {
    // Выделение на стеке
    let stack_value = 42;
    
    // Выделение в куче через Box
    let heap_value = Box::new(42);
    
    // Передача владения
    let moved_box = heap_value;
    // heap_value больше недоступен
}
}
graph TD
    subgraph "Кадр стека"
        SV["stack_value: 42"]
        BP["heap_value: Box<i32>"]
        BP2["moved_box: Box<i32>"]
    end
    
    subgraph "Куча"
        HV["42"]
    end
    
    BP -->|"Владеет"| HV
    BP -.->|"Передача владения"| BP2
    BP2 -->|"Теперь владеет"| HV
    
    subgraph "После перемещения"
        BP_X["heap_value: [ВНИМАНИЕ] ПЕРЕМЕЩЕНО"]
        BP2_A["moved_box: Box<i32>"]
    end
    
    BP2_A -->|"Владеет"| HV
    
    style BP_X fill:#ff6b6b,color:#000
    style HV fill:#91e5a3,color:#000
    style BP2_A fill:#51cf66,color:#000

Визуализация операций со срезами

#![allow(unused)]
fn main() {
fn slice_operations() {
    let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
    
    let full_slice = &data[..];        // [1,2,3,4,5,6,7,8]
    let partial_slice = &data[2..6];   // [3,4,5,6]
    let from_start = &data[..4];       // [1,2,3,4]
    let to_end = &data[3..];           // [4,5,6,7,8]
}
}
graph TD
    V["Vec: [1, 2, 3, 4, 5, 6, 7, 8]"]
    V --> FS["&data[..] → все элементы"]
    V --> PS["&data[2..6] → [3, 4, 5, 6]"]
    V --> SS["&data[..4] → [1, 2, 3, 4]"]
    V --> ES["&data[3..] → [4, 5, 6, 7, 8]"]
    
    style V fill:#e3f2fd,color:#000
    style FS fill:#91e5a3,color:#000
    style PS fill:#91e5a3,color:#000
    style SS fill:#91e5a3,color:#000
    style ES fill:#91e5a3,color:#000

Другие ключевые преимущества и возможности Rust

  • Никаких гонок данных между потоками (проверка Send/Sync на этапе компиляции)
  • Никакого использования после перемещения (в отличие от std::move в C++, который оставляет зомби-объекты)
  • Никаких неинициализированных переменных
    • Все переменные должны быть инициализированы до использования
  • Никаких тривиальных утечек памяти
    • Трейт Drop = RAII, сделанный правильно, правило пяти не нужно
    • Компилятор автоматически освобождает память, когда она выходит из области видимости
  • Никаких забытых блокировок мьютексов
    • Защитники блокировок — единственный способ доступа к данным (Mutex<T> оборачивает данные, а не доступ к ним)
  • Никакой сложности обработки исключений
    • Ошибки — это значения (Result<T, E>), видны в сигнатурах функций и распространяются с помощью ?
  • Отличная поддержка вывода типов, перечислений, сопоставления с образцом и абстракций с нулевой стоимостью
  • Встроенная поддержка управления зависимостями, сборки, тестирования, форматирования и линтинга
    • cargo заменяет make/CMake и фреймворки для линтинга и тестирования

Краткий справочник: Rust и C/C++

КонцепцияCC++RustКлючевое отличие
Управление памятьюmalloc()/free()unique_ptr, shared_ptrBox<T>, Rc<T>, Arc<T>Автоматическое, без циклов
Массивыint arr[10]std::vector<T>, std::array<T>Vec<T>, [T; N]Проверка границ по умолчанию
Строкиchar* с \0std::string, string_viewString, &strГарантированный UTF-8, проверка времён жизни
Ссылкиint* ptrT&, T&& (перемещение)&T, &mut TПроверка заимствований, времена жизни
ПолиморфизмУказатели на функцииВиртуальные функции, наследованиеТрейты, трейт-объектыКомпозиция вместо наследования
Обобщённое программированиеМакросы (void*)ШаблоныОбобщения + ограничения трейтамиПонятные сообщения об ошибках
Обработка ошибокКоды возврата, errnoИсключения, std::optionalResult<T, E>, Option<T>Никакого скрытого потока управления
Безопасность при nullptr == NULLnullptr, std::optional<T>Option<T>Принудительная проверка на null
ПотокобезопасностьВручную (pthreads)Вручную (std::mutex и др.)Гарантии на этапе компиляцииГонки данных невозможны
Система сборкиMake, CMakeCMake, Make и др.CargoВстроенный инструментарий
Неопределённое поведениеОшибки во время выполненияТонкое (переполнение знаковых, алиасинг)Ошибки на этапе компиляцииБезопасность гарантирована