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

Rust для программистов C#: полное учебное руководство

Подробное руководство по изучению Rust для разработчиков, которые уже знакомы с C#. Книга охватывает всё: от базового синтаксиса до продвинутых паттернов. Основной акцент сделан на концептуальных сдвигах и практических различиях между двумя языками.

Обзор курса

  • Аргументы в пользу Rust — зачем Rust важен для разработчиков C#: производительность, безопасность и корректность
  • Начало работы — установка, инструменты и первая программа
  • Базовые строительные блоки — типы, переменные, управление потоком выполнения
  • Структуры данных — массивы, кортежи, структуры, коллекции
  • Сопоставление с образцом и перечисления — алгебраические типы данных и исчерпывающее сопоставление
  • Владение и заимствование — модель управления памятью Rust
  • Модули и крейты — организация кода и зависимости
  • Обработка ошибок — распространение ошибок на основе Result
  • Трейты и обобщения — система типов Rust
  • Замыкания и итераторы — паттерны функционального программирования
  • Конкурентность — конкурентность без страха благодаря гарантиям системы типов, глубокое погружение в async/await
  • Unsafe Rust и FFI — когда и как выходить за рамки безопасного Rust
  • Паттерны миграции — реальные паттерны перехода с C# на Rust и постепенное внедрение
  • Лучшие практики — идиоматичный Rust для разработчиков C#

Руководство для самостоятельного изучения

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

Рекомендации по темпу:

ГлавыТемаПримерное времяКонтрольная точка
1–4Настройка, типы, управление потоком выполнения1 деньВы можете написать CLI-конвертер температур на Rust
5–6Структуры данных, перечисления, сопоставление с образцом1–2 дняВы можете определить перечисление с данными и исчерпывающе разобрать его через match
7Владение и заимствование1–2 дняВы можете объяснить, почему let s2 = s1 делает s1 недействительным
8–9Модули, обработка ошибок1 деньВы можете создать многофайловый проект, который передаёт ошибки дальше с помощью ?
10–12Трейты, обобщения, замыкания, итераторы1–2 дняВы можете перевести цепочку LINQ в итераторы Rust
13Конкурентность и async1 деньВы можете написать потокобезопасный счётчик с Arc<Mutex<T>>
14Unsafe Rust, FFI, тестирование1 деньВы можете вызвать функцию Rust из C# через P/Invoke
15–16Миграция, лучшие практики, инструментыВ своём темпеСправочный материал — обращайтесь к нему при написании реального кода
17Итоговый проект1–2 дняУ вас есть работающая CLI-утилита, которая получает данные о погоде

Как работать с упражнениями:

  • В главах есть практические упражнения в сворачиваемых блоках <details> с решениями
  • Всегда сначала попробуйте решить упражнение и только потом раскрывайте решение. Борьба с проверщиком заимствований — часть обучения, а сообщения компилятора — ваш учитель
  • Если вы застряли больше чем на 15 минут, раскройте решение, изучите его, затем закройте и попробуйте снова с нуля
  • Rust Playground позволяет запускать код без локальной установки

Индикаторы сложности:

  • 🟢 Начальный — прямой перевод концепций из C#
  • 🟡 Средний — требует понимания владения или трейтов
  • 🔴 Продвинутый — времена жизни, внутреннее устройство async или unsafe-код

Если вы упёрлись в стену:

  • Внимательно читайте сообщение компилятора: ошибки Rust исключительно полезны
  • Перечитайте нужный раздел. Концепции вроде владения (гл. 7) часто становятся понятны при втором прочтении
  • Документация стандартной библиотеки Rust отличная: ищите любой тип или метод
  • Более глубокие асинхронные паттерны описаны в сопутствующем курсе Async Rust

Оглавление

Часть I — Основы

1. Введение и мотивация 🟢

2. Начало работы 🟢

3. Встроенные типы и переменные 🟢

4. Управление потоком выполнения 🟢

5. Структуры данных и коллекции 🟢

6. Перечисления и сопоставление с образцом 🟡

7. Владение и заимствование 🟡

8. Крейты и модули 🟢

9. Обработка ошибок 🟡

10. Трейты и обобщения 🟡

11. Трейты From и Into 🟡

12. Замыкания и итераторы 🟡


Часть II — Конкурентность и системное программирование

13. Конкурентность 🔴

14. Unsafe Rust, FFI и тестирование 🟡


Часть III — Миграция и лучшие практики

15. Паттерны миграции и разборы случаев 🟡

16. Лучшие практики и справочник 🟡


Итоговый проект

17. Итоговый проект 🟡

1. Введение и мотивация

О курсе и общий подход

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

Аргументы в пользу Rust для разработчиков C#

Что вы узнаете: почему Rust важен для разработчиков C# — разрыв в производительности между управляемым и нативным кодом, как Rust на этапе компиляции устраняет ошибки NullReferenceException и скрытые потоки управления, а также ключевые сценарии, в которых Rust дополняет C# или заменяет его.

Сложность: 🟢 Начальный

Производительность без налога на рантайм

// C# — отличная продуктивность, но накладные расходы рантайма
public class DataProcessor
{
    private List<int> data = new List<int>();
    
    public void ProcessLargeDataset()
    {
        // Выделения памяти запускают GC
        for (int i = 0; i < 10_000_000; i++)
        {
            data.Add(i * 2); // Нагрузка на GC
        }
        // Непредсказуемые паузы GC во время обработки
    }
}
// Время выполнения: переменное (50–200 мс из-за GC)
// Память: ~80 МБ (включая накладные расходы GC)
// Предсказуемость: низкая (паузы GC)
#![allow(unused)]
fn main() {
// Rust — та же выразительность, нулевые накладные расходы рантайма
struct DataProcessor {
    data: Vec<i32>,
}

impl DataProcessor {
    fn process_large_dataset(&mut self) {
        // Абстракции с нулевой стоимостью
        for i in 0..10_000_000 {
            self.data.push(i * 2); // Нет нагрузки на GC
        }
        // Детерминированная производительность
    }
}
// Время выполнения: стабильное (~30 мс)
// Память: ~40 МБ (точное выделение)
// Предсказуемость: высокая (нет GC)
}

Безопасность памяти без проверок во время выполнения

// C# — безопасность во время выполнения с накладными расходами
public class RuntimeCheckedOperations
{
    public string? ProcessArray(int[] array)
    {
        // Проверка границ во время выполнения при каждом обращении
        if (array.Length > 0)
        {
            return array[0].ToString(); // Безопасно — int это значимый тип, он никогда не бывает null
        }
        return null; // Nullable-возвращаемое значение (string? с nullable-ссылочными типами C# 8+)
    }
    
    public void ProcessConcurrently()
    {
        var list = new List<int>();
        
        // Гонки данных возможны, нужна аккуратная синхронизация
        Parallel.For(0, 1000, i =>
        {
            lock (list) // Накладные расходы рантайма
            {
                list.Add(i);
            }
        });
    }
}
#![allow(unused)]
fn main() {
// Rust — безопасность на этапе компиляции без затрат во время выполнения
struct SafeOperations;

impl SafeOperations {
    // Null-безопасность на этапе компиляции, без проверок во время выполнения
    fn process_array(array: &[i32]) -> Option<String> {
        array.first().map(|x| x.to_string())
        // Null-ссылки здесь невозможны
        // Проверка границ убирается оптимизатором, когда безопасность доказуема
    }
    
    fn process_concurrently() {
        use std::sync::{Arc, Mutex};
        use std::thread;
        
        let data = Arc::new(Mutex::new(Vec::new()));
        
        // Гонки данных предотвращаются на этапе компиляции
        let handles: Vec<_> = (0..1000).map(|i| {
            let data = Arc::clone(&data);
            thread::spawn(move || {
                data.lock().unwrap().push(i);
            })
        }).collect();
        
        for handle in handles {
            handle.join().unwrap();
        }
    }
}
}

Типичные болевые точки C#, которые решает Rust

1. «Миллиардная ошибка»: null-ссылки

// C# — исключения NullReferenceException это мины замедленного действия во время выполнения
public class UserService
{
    public string GetUserDisplayName(User user)
    {
        // Любое из этих обращений может бросить NullReferenceException
        return user.Profile.DisplayName.ToUpper();
        //     ^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^^^^^^^
        //     Во время выполнения любое из них может оказаться null
    }
    
    // Nullable-ссылочные типы (C# 8+) помогают, но null всё равно может проскочить
    public string GetDisplayName(User? user)
    {
        return user?.Profile?.DisplayName?.ToUpper() ?? "Unknown";
        // Эта конкретная строка безопасна благодаря ?. и ??,
        // но NRT носят рекомендательный характер — компилятор можно обойти с помощью `!`
    }
}
#![allow(unused)]
fn main() {
// Rust — null-безопасность гарантируется на этапе компиляции
struct UserService;

impl UserService {
    fn get_user_display_name(user: &User) -> Option<String> {
        user.profile.as_ref()?
            .display_name.as_ref()
            .map(|name| name.to_uppercase())
        // Компилятор заставляет обработать случай None
        // Null-указатели и их исключения здесь невозможны
    }
    
    fn get_display_name_safe(user: Option<&User>) -> String {
        user.and_then(|u| u.profile.as_ref())
            .and_then(|p| p.display_name.as_ref())
            .map(|name| name.to_uppercase())
            .unwrap_or_else(|| "Unknown".to_string())
        // Явная обработка, никаких сюрпризов
    }
}
}

2. Скрытые исключения и управление потоком

// C# — исключения могут бросаться откуда угодно
public async Task<UserData> GetUserDataAsync(int userId)
{
    // Каждый из этих вызовов может бросить разные исключения
    var user = await userRepository.GetAsync(userId);        // SqlException
    var permissions = await permissionService.GetAsync(user); // HttpRequestException  
    var preferences = await preferenceService.GetAsync(user); // TimeoutException
    
    return new UserData(user, permissions, preferences);
    // Вызывающий код понятия не имеет, какие исключения ожидать
}
#![allow(unused)]
fn main() {
// Rust — все ошибки явно указаны в сигнатурах функций
#[derive(Debug)]
enum UserDataError {
    DatabaseError(String),
    NetworkError(String),
    Timeout,
    UserNotFound(i32),
}

async fn get_user_data(user_id: i32) -> Result<UserData, UserDataError> {
    // Все ошибки явные и обработаны
    let user = user_repository.get(user_id).await
        .map_err(UserDataError::DatabaseError)?;
    
    let permissions = permission_service.get(&user).await
        .map_err(UserDataError::NetworkError)?;
    
    let preferences = preference_service.get(&user).await
        .map_err(|_| UserDataError::Timeout)?;
    
    Ok(UserData::new(user, permissions, preferences))
    // Вызывающий код точно знает, какие ошибки возможны
}
}

3. Корректность: система типов как движок доказательств

Система типов Rust отлавливает целые классы логических ошибок на этапе компиляции — тогда как C# может поймать их только во время выполнения, или не поймать вовсе.

ADT против обходных путей через sealed-классы

// C# — дискриминируемые объединения требуют шаблонного кода с sealed-классами.
// Компилятор предупреждает о пропущенных случаях (CS8524) ТОЛЬКО если нет catch-all `_`.
// На практике большинство C#-кода использует `_` по умолчанию, и это заглушает предупреждение.
public abstract record Shape;
public sealed record Circle(double Radius)   : Shape;
public sealed record Rectangle(double W, double H) : Shape;
public sealed record Triangle(double A, double B, double C) : Shape;

public static double Area(Shape shape) => shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.W * r.H,
    // Забыли Triangle? Паттерн `_` заглушает любое предупреждение компилятора.
    _           => throw new ArgumentException("Unknown shape")
};
// Через полгода добавляете новый вариант — паттерн `_` скрывает пропущенный случай.
// Ни одно предупреждение компилятора не скажет о 47 switch-выражениях, которые нужно обновить.
#![allow(unused)]
fn main() {
// Rust — ADT + исчерпывающее сопоставление = доказательство на этапе компиляции
enum Shape {
    Circle { radius: f64 },
    Rectangle { w: f64, h: f64 },
    Triangle { a: f64, b: f64, c: f64 },
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius }    => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { w, h }   => w * h,
        // Забыли Triangle? ОШИБКА: сопоставление не исчерпывающее
        Shape::Triangle { a, b, c } => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}
// Добавляете новый вариант → компилятор покажет ВСЕ сопоставления, которые нужно обновить.
}

Неизменяемость по умолчанию против неизменяемости по выбору

// C# — всё изменяемо по умолчанию
public class Config
{
    public string Host { get; set; }   // Изменяемо по умолчанию
    public int Port { get; set; }
}

// "readonly" и "record" помогают, но не предотвращают глубокую мутацию:
public record ServerConfig(string Host, int Port, List<string> AllowedOrigins);

var config = new ServerConfig("localhost", 8080, new List<string> { "*.example.com" });
// Records «неизменяемы», но поля ссылочных типов — НЕТ:
config.AllowedOrigins.Add("*.evil.com"); // Компилируется и мутирует! ← баг
// Компилятор не выдаёт ни одного предупреждения.
#![allow(unused)]
fn main() {
// Rust — неизменяемо по умолчанию, мутация явная и видимая
struct Config {
    host: String,
    port: u16,
    allowed_origins: Vec<String>,
}

let config = Config {
    host: "localhost".into(),
    port: 8080,
    allowed_origins: vec!["*.example.com".into()],
};

// config.allowed_origins.push("*.evil.com".into()); // ОШИБКА: нельзя заимствовать как изменяемое

// Мутация требует явного согласия:
let mut config = config;
config.allowed_origins.push("*.safe.com".into()); // OK — мутация видна

// «mut» в сигнатуре говорит каждому читателю: «эта функция изменяет данные»
fn add_origin(config: &mut Config, origin: String) {
    config.allowed_origins.push(origin);
}
}

Функциональное программирование: полноправная возможность или запоздалая мысль

// C# — ФП прикручено сбоку; LINQ выразителен, но язык сопротивляется
public IEnumerable<Order> GetHighValueOrders(IEnumerable<Order> orders)
{
    return orders
        .Where(o => o.Total > 1000)   // Func<Order, bool> — делегат в куче
        .Select(o => new OrderSummary  // Анонимный тип или дополнительный класс
        {
            Id = o.Id,
            Total = o.Total
        })
        .OrderByDescending(o => o.Total);
    // Нет исчерпывающего сопоставления результатов
    // Null может проскользнуть куда угодно в конвейере
    // Нельзя обеспечить чистоту — любая лямбда может иметь побочные эффекты
}
#![allow(unused)]
fn main() {
// Rust — ФП является полноправной частью языка
fn get_high_value_orders(orders: &[Order]) -> Vec<OrderSummary> {
    orders.iter()
        .filter(|o| o.total > 1000)      // Замыкание с нулевой стоимостью, без выделения в куче
        .map(|o| OrderSummary {           // Структура с проверкой типов
            id: o.id,
            total: o.total,
        })
        .sorted_by(|a, b| b.total.cmp(&a.total)) // itertools
        .collect()
    // Нигде в конвейере нет null
    // Замыкания мономорфизируются — нулевые накладные расходы по сравнению с написанными вручную циклами
    // Чистота обеспечивается: &[Order] означает, что функция НЕ может изменить orders
}
}

Наследование: элегантно в теории, хрупко на практике

// C# — проблема хрупкого базового класса
public class Animal
{
    public virtual string Speak() => "...";
    public void Greet() => Console.WriteLine($"I say: {Speak()}");
}

public class Dog : Animal
{
    public override string Speak() => "Woof!";
}

public class RobotDog : Dog
{
    // Какой Speak() вызовет Greet()? Что если Dog изменится?
    // Ромбовидная проблема с интерфейсами + методами по умолчанию
    // Жёсткая связанность: изменение Animal может тихо сломать RobotDog
}

// Распространённые антипаттерны C#:
// - «Божественные» базовые классы с 20 виртуальными методами
// - Глубокие иерархии (5+ уровней), в которых никто не может разобраться
// - Поля "protected", создающие скрытую связанность
// - Изменения базового класса, которые молча меняют поведение производных
#![allow(unused)]
fn main() {
// Rust — композиция вместо наследования, обеспечивается самим языком
trait Speaker {
    fn speak(&self) -> &str;
}

trait Greeter: Speaker {
    fn greet(&self) {
        println!("I say: {}", self.speak());
    }
}

struct Dog;
impl Speaker for Dog {
    fn speak(&self) -> &str { "Woof!" }
}
impl Greeter for Dog {} // Использует greet() по умолчанию

struct RobotDog {
    voice: String, // Композиция: владеет собственными данными
}
impl Speaker for RobotDog {
    fn speak(&self) -> &str { &self.voice }
}
impl Greeter for RobotDog {} // Понятное и явное поведение

// Нет проблемы хрупкого базового класса — базовых классов вообще нет
// Нет скрытой связанности — трейты это явные контракты
// Нет ромбовидной проблемы — правила согласованности трейтов исключают неоднозначность
// Добавляете метод в Speaker? Компилятор покажет все места, где его нужно реализовать.
}

Ключевая мысль: в C# корректность — это дисциплина: вы надеетесь, что разработчики следуют соглашениям, пишут тесты и ловят пограничные случаи на код-ревью. В Rust корректность — свойство системы типов: целые классы ошибок (разыменование null, забытые варианты, случайная мутация, гонки данных) структурно невозможны.


4. Непредсказуемая производительность из-за GC

// C# — GC может приостановить работу в любой момент
public class HighFrequencyTrader
{
    private List<Trade> trades = new List<Trade>();
    
    public void ProcessMarketData(MarketTick tick)
    {
        // Выделения памяти могут запустить GC в худший возможный момент
        var analysis = new MarketAnalysis(tick);
        trades.Add(new Trade(analysis.Signal, tick.Price));
        
        // GC может приостановить работу прямо здесь, в критический момент рынка
        // Длительность паузы: 1–100 мс в зависимости от размера кучи
    }
}
#![allow(unused)]
fn main() {
// Rust — предсказуемая, детерминированная производительность
struct HighFrequencyTrader {
    trades: Vec<Trade>,
}

impl HighFrequencyTrader {
    fn process_market_data(&mut self, tick: MarketTick) {
        // Извлекаем поле Copy до того, как переместим `tick` в analysis
        let price = tick.price;

        // Нулевые выделения, предсказуемая производительность
        let analysis = MarketAnalysis::from(tick);
        self.trades.push(Trade::new(analysis.signal(), price));
        
        // Никаких пауз GC, стабильная задержка на уровне субмикросекунд
        // Производительность гарантируется системой типов
    }
}
}

Когда выбирать Rust вместо C#

✅ Выбирайте Rust, когда:

  • Важна корректность: конечные автоматы, реализации протоколов, финансовая логика — там, где пропущенный случай становится инцидентом в продакшене, а не провалом теста
  • Критична производительность: системы реального времени, высокочастотная торговля, игровые движки
  • Важно потребление памяти: встраиваемые системы, облачные затраты, мобильные приложения
  • Требуется предсказуемость: медицинское оборудование, автомобильная отрасль, финансовые системы
  • Безопасность превыше всего: криптография, сетевая безопасность, низкоуровневый код
  • Долгоживущие сервисы: там, где паузы GC создают проблемы
  • Ограниченные ресурсы: IoT, edge-вычисления
  • Системное программирование: CLI-утилиты, базы данных, веб-серверы, операционные системы

✅ Оставайтесь на C#, когда:

  • Быстрая разработка приложений: бизнес-приложения, CRUD-приложения
  • Большая существующая кодовая база: когда стоимость миграции непомерна
  • Экспертиза команды: когда кривая обучения Rust не окупается выгодами
  • Корпоративные интеграции: сильные зависимости от .NET Framework/Windows
  • GUI-приложения: экосистемы WPF, WinUI, Blazor
  • Скорость выхода на рынок: когда скорость разработки важнее производительности

🔄 Рассмотрите оба варианта (гибридный подход):

  • Компоненты, критичные к производительности, на Rust: вызываются из C# через P/Invoke
  • Бизнес-логика на C#: знакомая и продуктивная разработка
  • Постепенная миграция: начните с новых сервисов на Rust

Влияние на практике: почему компании выбирают Rust

Dropbox: инфраструктура хранения

  • До (Python): высокая загрузка CPU, накладные расходы памяти
  • После (Rust): прирост производительности в 10 раз, снижение потребления памяти на 50%
  • Результат: миллионы экономии на инфраструктуре

Discord: бэкенд голоса и видео

  • До (Go): паузы GC, приводившие к обрывам аудио
  • После (Rust): стабильно низкая задержка
  • Результат: лучший пользовательский опыт, меньше затрат на серверы

Microsoft: компоненты Windows

  • Rust в Windows: компоненты файловой системы и сетевого стека
  • Преимущество: безопасность памяти без потери производительности
  • Эффект: меньше уязвимостей безопасности при той же производительности

Почему это важно для разработчиков C#:

  1. Взаимодополняющие навыки: Rust и C# решают разные задачи
  2. Карьерный рост: экспертиза в системном программировании всё более ценна
  3. Понимание производительности: изучение абстракций с нулевой стоимостью
  4. Мышление о безопасности: применяйте мышление о владении в любом языке
  5. Облачные затраты: производительность напрямую влияет на расходы на инфраструктуру

Сравнение философий языков

Философия C#

  • Продуктивность прежде всего: богатые инструменты, обширный фреймворк, «яма успеха» (pit of success)
  • Управляемый рантайм: сборщик мусора автоматически управляет памятью
  • Ориентация на корпоративный сегмент: строгая типизация с рефлексией, обширная стандартная библиотека
  • Объектно-ориентированность: классы, наследование, интерфейсы как основные абстракции

Философия Rust

  • Производительность без компромиссов: абстракции с нулевой стоимостью, без накладных расходов рантайма
  • Безопасность памяти: гарантии на этапе компиляции предотвращают сбои и уязвимости безопасности
  • Системное программирование: прямой доступ к оборудованию с высокоуровневыми абстракциями
  • Функциональность + системность: неизменяемость по умолчанию, управление ресурсами на основе владения
graph TD
    subgraph "Модель разработки C#"
        CS_CODE["Исходный код C#<br/>Классы, методы, свойства"]
        CS_COMPILE["Компилятор C#<br/>(csc.exe)"]
        CS_IL["Промежуточный язык<br/>(байт-код IL)"]
        CS_RUNTIME[".NET Runtime<br/>(CLR)"]
        CS_JIT["Just-In-Time компилятор"]
        CS_NATIVE["Машинный код"]
        CS_GC["Сборщик мусора<br/>(управление памятью)"]
        
        CS_CODE --> CS_COMPILE
        CS_COMPILE --> CS_IL
        CS_IL --> CS_RUNTIME
        CS_RUNTIME --> CS_JIT
        CS_JIT --> CS_NATIVE
        CS_RUNTIME --> CS_GC
        
        CS_BENEFITS["[OK] Быстрая разработка<br/>[OK] Богатая экосистема<br/>[OK] Автоматическое управление памятью<br/>[ERROR] Накладные расходы рантайма<br/>[ERROR] Паузы GC<br/>[ERROR] Зависимость от платформы"]
    end
    
    subgraph "Модель разработки Rust"
        RUST_CODE["Исходный код Rust<br/>Структуры, перечисления, функции"]
        RUST_COMPILE["Компилятор Rust<br/>(rustc)"]
        RUST_NATIVE["Машинный код<br/>(прямая компиляция)"]
        RUST_ZERO["Нулевой рантайм<br/>(нет VM, нет GC)"]
        
        RUST_CODE --> RUST_COMPILE
        RUST_COMPILE --> RUST_NATIVE
        RUST_NATIVE --> RUST_ZERO
        
        RUST_BENEFITS["[OK] Максимальная производительность<br/>[OK] Безопасность памяти<br/>[OK] Нет зависимостей рантайма<br/>[ERROR] Более крутая кривая обучения<br/>[ERROR] Более долгая сборка<br/>[ERROR] Более явный код"]
    end
    
    style CS_BENEFITS fill:#e3f2fd,color:#000
    style RUST_BENEFITS fill:#e8f5e8,color:#000
    style CS_GC fill:#fff3e0,color:#000
    style RUST_ZERO fill:#e8f5e8,color:#000

Краткая справка: Rust против C#

КонцепцияC#RustКлючевое отличие
Управление памятьюСборщик мусораСистема владенияНулевая стоимость, детерминированная очистка
Null-ссылкиnull повсюдуOption<T>Null-безопасность на этапе компиляции
Обработка ошибокИсключенияResult<T, E>Явно, без скрытого управления потоком
ИзменяемостьИзменяемо по умолчаниюНеизменяемо по умолчаниюМутация по явному выбору
Система типовСсылочные/значимые типыТипы владенияСемантика перемещения, заимствование
СборкиGAC, домены приложений (.NET Framework); side-by-side (.NET 5+)КрейтыСтатическая линковка, без рантайма
Пространства имёнusing System.IOuse std::fsСистема модулей
Интерфейсыinterface IFootrait FooРеализации по умолчанию
ОбобщенияList<T> (необязательные ограничения через where)Vec<T> (ограничения трейтов вида T: Clone)Абстракции с нулевой стоимостью
МногопоточностьБлокировки, async/awaitВладение + Send/SyncПредотвращение гонок данных
ПроизводительностьJIT-компиляцияAOT-компиляцияПредсказуемо, без пауз GC

2. Начало работы

Установка и настройка

Что вы узнаете: как установить Rust и настроить IDE, чем система сборки Cargo отличается от MSBuild/NuGet, как выглядит первая программа на Rust по сравнению с C#, и как читать ввод из командной строки.

Сложность: 🟢 Начальный

Установка Rust

# Установка Rust (работает на Windows, macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# В Windows можно также скачать с: https://rustup.rs/

Инструменты Rust против инструментов C#

Инструмент C#Аналог в RustНазначение
dotnet newcargo newСоздание нового проекта
dotnet buildcargo buildКомпиляция проекта
dotnet runcargo runЗапуск проекта
dotnet testcargo testЗапуск тестов
NuGetCrates.ioРепозиторий пакетов
MSBuildCargoСистема сборки
Visual StudioVS Code + rust-analyzerIDE

Настройка IDE

  1. VS Code (рекомендуется для начинающих)

    • Установите расширение «rust-analyzer»
    • Установите «CodeLLDB» для отладки
  2. Visual Studio (Windows)

    • Установите расширение с поддержкой Rust
  3. JetBrains RustRover (полноценная IDE)

    • Похожа на Rider для C#

Первая программа на Rust

Hello World на C#

// Program.cs
using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

Hello World на Rust

// main.rs
fn main() {
    println!("Hello, World!");
}

Ключевые отличия для разработчиков C#

  1. Классы не требуются — функции могут существовать на верхнем уровне
  2. Пространств имён нет — вместо них используется система модулей
  3. println! — это макрос — обратите внимание на !
  4. Точка с запятой важна — отсутствие завершающей точки с запятой превращает инструкцию в возвращаемое выражение
  5. Явного типа возвращаемого значения нет — main возвращает () (тип unit)

Создание первого проекта

# Создание нового проекта (аналог 'dotnet new console')
cargo new hello_rust
cd hello_rust

# Созданная структура проекта:
# hello_rust/
# ├── Cargo.toml      (аналог файла .csproj)
# └── src/
#     └── main.rs     (аналог Program.cs)

# Запуск проекта (аналог 'dotnet run')
cargo run

Cargo против NuGet/MSBuild

Конфигурация проекта

C# (.csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  <PackageReference Include="Serilog" Version="3.0.1" />
</Project>

Rust (Cargo.toml)

[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"

[dependencies]
serde_json = "1.0"    # Аналог Newtonsoft.Json
log = "0.4"           # Аналог Serilog

Основные команды Cargo

# Создание нового проекта
cargo new my_project
cargo new my_project --lib  # Создать библиотечный проект

# Сборка и запуск
cargo build          # Аналог 'dotnet build'
cargo run            # Аналог 'dotnet run'
cargo test           # Аналог 'dotnet test'

# Управление пакетами
cargo add serde      # Добавить зависимость (аналог 'dotnet add package')
cargo update         # Обновить зависимости

# Релизная сборка
cargo build --release  # Оптимизированная сборка
cargo run --release    # Запуск оптимизированной версии

# Документация
cargo doc --open     # Сгенерировать и открыть документацию

Workspace против Solution

Решение C# (.sln)

MySolution/
├── MySolution.sln
├── WebApi/
│   └── WebApi.csproj
├── Business/
│   └── Business.csproj
└── Tests/
    └── Tests.csproj

Рабочее пространство Rust (Cargo.toml)

[workspace]
members = [
    "web_api",
    "business", 
    "tests"
]

Чтение ввода и аргументов командной строки

Каждый разработчик C# знает Console.ReadLine(). Вот как в Rust обрабатывать пользовательский ввод, переменные окружения и аргументы командной строки.

Ввод с консоли

// C# — чтение пользовательского ввода
Console.Write("Введите ваше имя: ");
string? name = Console.ReadLine();  // Возвращает string? начиная с .NET 6+
Console.WriteLine($"Привет, {name}!");

// Разбор ввода
Console.Write("Введите число: ");
if (int.TryParse(Console.ReadLine(), out int number))
{
    Console.WriteLine($"Вы ввели: {number}");
}
else
{
    Console.WriteLine("Это не число.");
}
use std::io::{self, Write};

fn main() {
    // Чтение строки ввода
    print!("Введите ваше имя: ");
    io::stdout().flush().unwrap(); // flush нужен, потому что print! не сбрасывает буфер автоматически

    let mut name = String::new();
    io::stdin().read_line(&mut name).expect("Не удалось прочитать строку");
    let name = name.trim(); // убираем завершающий перенос строки
    println!("Привет, {name}!");

    // Разбор ввода
    print!("Введите число: ");
    io::stdout().flush().unwrap();

    let mut input = String::new();
    io::stdin().read_line(&mut input).expect("Не удалось прочитать");
    match input.trim().parse::<i32>() {
        Ok(number) => println!("Вы ввели: {number}"),
        Err(_)     => println!("Это не число."),
    }
}

Аргументы командной строки

// C# — чтение аргументов CLI
static void Main(string[] args)
{
    if (args.Length < 1)
    {
        Console.WriteLine("Использование: program <filename>");
        return;
    }
    string filename = args[0];
    Console.WriteLine($"Обработка {filename}");
}
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    //  args[0] = имя программы (аналог имени сборки в C#)
    //  args[1..] = собственно аргументы

    if args.len() < 2 {
        eprintln!("Использование: {} <filename>", args[0]); // eprintln! → stderr
        std::process::exit(1);
    }
    let filename = &args[1];
    println!("Обработка {filename}");
}

Переменные окружения

// C#
string dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "localhost";
#![allow(unused)]
fn main() {
use std::env;

let db_url = env::var("DATABASE_URL").unwrap_or_else(|_| "localhost".to_string());
// env::var возвращает Result<String, VarError> — никаких null!
}

Промышленные CLI-приложения с clap

Для всего, что выходит за рамки тривиального разбора аргументов, используйте крейт clap — это аналог System.CommandLine или библиотек вроде CommandLineParser в Rust.

# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;

/// Простой обработчик файлов — этот doc-комментарий станет текстом справки
#[derive(Parser, Debug)]
#[command(name = "processor", version, about)]
struct Args {
    /// Входной файл для обработки
    #[arg(short, long)]
    input: String,

    /// Выходной файл (по умолчанию — stdout)
    #[arg(short, long)]
    output: Option<String>,

    /// Включить подробное логирование
    #[arg(short, long, default_value_t = false)]
    verbose: bool,

    /// Количество рабочих потоков
    #[arg(short = 'j', long, default_value_t = 4)]
    threads: usize,
}

fn main() {
    let args = Args::parse(); // автоматически разбирает, проверяет и генерирует --help

    if args.verbose {
        println!("Вход:    {}", args.input);
        println!("Выход:   {:?}", args.output);
        println!("Потоки:  {}", args.threads);
    }

    // Используйте args.input, args.output и т.д.
}
# Автоматически сгенерированная справка:
$ processor --help
Простой обработчик файлов

Usage: processor [OPTIONS] --input <INPUT>

Options:
  -i, --input <INPUT>      Входной файл для обработки
  -o, --output <OUTPUT>    Выходной файл (по умолчанию — stdout)
  -v, --verbose            Включить подробное логирование
  -j, --threads <THREADS>  Количество рабочих потоков [default: 4]
  -h, --help               Print help
  -V, --version            Print version
// Эквивалент на C# с System.CommandLine (больше шаблонного кода):
var inputOption = new Option<string>("--input", "Входной файл") { IsRequired = true };
var verboseOption = new Option<bool>("--verbose", "Включить подробное логирование");
var rootCommand = new RootCommand("Простой обработчик файлов");
rootCommand.AddOption(inputOption);
rootCommand.AddOption(verboseOption);
rootCommand.SetHandler((input, verbose) => { /* ... */ }, inputOption, verboseOption);
await rootCommand.InvokeAsync(args);
// Подход с derive-макросом в clap короче и безопаснее по типам
C#RustПримечания
Console.ReadLine()io::stdin().read_line(&mut buf)Нужно передать буфер, возвращает Result
int.TryParse(s, out n)s.parse::<i32>()Возвращает Result<i32, ParseIntError>
args[0]env::args().nth(1)В Rust args[0] — имя программы
Environment.GetEnvironmentVariableenv::var("KEY")Возвращает Result, а не nullable-значение
System.CommandLineclapОснован на derive, автоматически генерирует справку

Справочник по ключевым словам (необязательно)

Ключевые слова Rust для разработчиков C#

Что вы узнаете: краткое соответствие ключевых слов Rust их аналогам в C# — модификаторы видимости, ключевые слова владения, управление потоком, определения типов и синтаксис сопоставления с образцом.

Сложность: 🟢 Начальный

Понимание ключевых слов Rust и их назначения помогает разработчикам C# эффективнее осваивать язык.

Ключевые слова видимости и контроля доступа

Модификаторы доступа C#

public class Example
{
    public int PublicField;           // Доступно везде
    private int privateField;        // Только внутри этого класса
    protected int protectedField;    // Этот класс и его наследники
    internal int internalField;      // Внутри этой сборки
    protected internal int protectedInternalField; // Комбинация
}

Ключевые слова видимости Rust

#![allow(unused)]
fn main() {
// pub - делает элементы публичными (как public в C#)
pub struct PublicStruct {
    pub public_field: i32,           // Публичное поле
    private_field: i32,              // По умолчанию приватное (без ключевого слова)
}

pub mod my_module {
    pub(crate) fn crate_public() {}     // Публично внутри текущего крейта (как internal)
    pub(super) fn parent_public() {}    // Публично для родительского модуля
    pub(self) fn self_public() {}       // Публично внутри текущего модуля (то же, что и приватное)
    
    pub use super::PublicStruct;        // Реэкспорт (как псевдоним через using)
}

// Прямого аналога protected в C# нет — используйте композицию
}

Ключевые слова памяти и владения

Ключевые слова памяти C#

// ref - передача по ссылке
public void Method(ref int value) { value = 10; }

// out - выходной параметр
public bool TryParse(string input, out int result) { /* */ }

// in - ссылка только для чтения (C# 7.2+)
public void ReadOnly(in LargeStruct data) { /* Нельзя изменить data */ }

Ключевые слова владения Rust

#![allow(unused)]
fn main() {
// & - неизменяемая ссылка (как in-параметр в C#)
fn read_only(data: &Vec<i32>) {
    println!("Длина: {}", data.len()); // Можно читать, нельзя изменять
}

// &mut - изменяемая ссылка (как ref-параметр в C#)
fn modify(data: &mut Vec<i32>) {
    data.push(42); // Можно изменять
}

// move - принудительный захват перемещением в замыканиях
let data = vec![1, 2, 3];
let closure = move || {
    println!("{:?}", data); // data перемещается в замыкание
};
// data здесь больше недоступен

// Box - выделение в куче (как new для ссылочных типов в C#)
let boxed_data = Box::new(42); // Выделяем в куче
}

Ключевые слова управления потоком

Управление потоком C#

// return - выход из функции со значением
public int GetValue() { return 42; }

// yield return - паттерн итератора
public IEnumerable<int> GetNumbers()
{
    yield return 1;
    yield return 2;
}

// break/continue - управление циклом
foreach (var item in items)
{
    if (item == null) continue;
    if (item.Stop) break;
}

Ключевые слова управления потоком Rust

#![allow(unused)]
fn main() {
// return - явный возврат (обычно не нужен)
fn get_value() -> i32 {
    return 42; // Явный возврат
    // ИЛИ просто: 42 (неявный возврат)
}

// break/continue - управление циклом с необязательными значениями
fn find_value() -> Option<i32> {
    loop {
        let value = get_next();
        if value < 0 { continue; }
        if value > 100 { break None; }      // Выход с значением
        if value == 42 { break Some(value); } // Выход с успешным результатом
    }
}

// loop - бесконечный цикл (как while(true))
loop {
    if condition { break; }
}

// while - условный цикл
while condition {
    // код
}

// for - цикл по итератору
for item in collection {
    // код
}
}

Ключевые слова определения типов

Ключевые слова типов C#

// class - ссылочный тип
public class MyClass { }

// struct - значимый тип
public struct MyStruct { }

// interface - определение контракта
public interface IMyInterface { }

// enum - перечисление
public enum MyEnum { Value1, Value2 }

// delegate - указатель на функцию
public delegate void MyDelegate(int value);

Ключевые слова типов Rust

#![allow(unused)]
fn main() {
// struct - структура данных (объединяет class и struct из C#)
struct MyStruct {
    field: i32,
}

// enum - алгебраический тип данных (гораздо мощнее enum из C#)
enum MyEnum {
    Variant1,
    Variant2(i32),              // Может хранить данные
    Variant3 { x: i32, y: i32 }, // Вариант в виде структуры
}

// trait - определение интерфейса (как interface в C#, но мощнее)
trait MyTrait {
    fn method(&self);
    
    // Реализация по умолчанию (как default-методы интерфейсов в C# 8+)
    fn default_method(&self) {
        println!("Реализация по умолчанию");
    }
}

// type - псевдоним типа (как псевдоним using в C#)
type UserId = u32;
type Result<T> = std::result::Result<T, MyError>;

// impl - блок реализации (аналога в C# нет — методы определяются отдельно)
impl MyStruct {
    fn new() -> MyStruct {
        MyStruct { field: 0 }
    }
}

impl MyTrait for MyStruct {
    fn method(&self) {
        println!("Реализация");
    }
}
}

Ключевые слова определения функций

Ключевые слова функций C#

// static - метод класса
public static void StaticMethod() { }

// virtual - может быть переопределён
public virtual void VirtualMethod() { }

// override - переопределение базового метода
public override void VirtualMethod() { }

// abstract - обязан быть реализован
public abstract void AbstractMethod();

// async - асинхронный метод
public async Task<int> AsyncMethod() { return await SomeTask(); }

Ключевые слова функций Rust

#![allow(unused)]
fn main() {
// fn - определение функции (как метод в C#, но существует самостоятельно)
fn regular_function() {
    println!("Привет");
}

// const fn - функция времени компиляции (как const в C#, но для функций)
const fn compile_time_function() -> i32 {
    42 // Может быть вычислена на этапе компиляции
}

// async fn - асинхронная функция (как async в C#)
async fn async_function() -> i32 {
    some_async_operation().await
}

// unsafe fn - функция, которая может нарушить безопасность памяти
unsafe fn unsafe_function() {
    // Может выполнять небезопасные операции
}

// extern fn - интерфейс внешних функций (FFI)
extern "C" fn c_compatible_function() {
    // Может вызываться из C
}
}

Ключевые слова объявления переменных

Ключевые слова переменных C#

// var - вывод типа
var name = "John"; // Выводится как string

// const - константа времени компиляции
const int MaxSize = 100;

// readonly - константа времени выполнения (только для полей, не для локальных переменных)
// readonly DateTime createdAt = DateTime.Now;

// static - переменная уровня класса
static int instanceCount = 0;

Ключевые слова переменных Rust

#![allow(unused)]
fn main() {
// let - привязка переменной (как var в C#)
let name = "John"; // По умолчанию неизменяемая

// let mut - изменяемая привязка переменной
let mut count = 0; // Может быть изменена
count += 1;

// const - константа времени компиляции (как const в C#)
const MAX_SIZE: usize = 100;

// static - глобальная переменная (как static в C#)
static INSTANCE_COUNT: std::sync::atomic::AtomicUsize = 
    std::sync::atomic::AtomicUsize::new(0);
}

Ключевые слова сопоставления с образцом

Сопоставление с образцом C# (C# 8+)

// switch-выражение
string result = value switch
{
    1 => "Один",
    2 => "Два",
    _ => "Другое"
};

// паттерн is
if (obj is string str)
{
    Console.WriteLine(str.Length);
}

Ключевые слова сопоставления с образцом Rust

#![allow(unused)]
fn main() {
// match - сопоставление с образцом (как switch в C#, но гораздо мощнее)
let result = match value {
    1 => "Один",
    2 => "Два",
    3..=10 => "От 3 до 10", // Диапазонные паттерны
    _ => "Другое", // Подстановочный паттерн (как _ в C#)
};

// if let - условное сопоставление с образцом
if let Some(value) = optional {
    println!("Получено значение: {}", value);
}

// while let - цикл с сопоставлением с образцом
while let Some(item) = iterator.next() {
    println!("Элемент: {}", item);
}

// let с паттернами - деструктуризация
let (x, y) = point; // Деструктуризация кортежа
let Some(value) = optional else {
    return; // Ранний выход, если паттерн не совпал
};
}

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

Ключевые слова памяти C#

// unsafe - отключение проверок безопасности
unsafe
{
    int* ptr = &variable;
    *ptr = 42;
}

// fixed - закрепление управляемой памяти
unsafe
{
    fixed (byte* ptr = array)
    {
        // Использовать ptr
    }
}

Ключевые слова безопасности Rust

#![allow(unused)]
fn main() {
// unsafe - отключение проверок заимствований (используйте осторожно!)
unsafe {
    let ptr = &variable as *const i32;
    let value = *ptr; // Разыменование сырого указателя
}

// Типы сырых указателей (аналога в C# нет — обычно не нужны)
let ptr: *const i32 = &42;  // Неизменяемый сырой указатель
let ptr: *mut i32 = &mut 42; // Изменяемый сырой указатель
}

Распространённые ключевые слова Rust, которых нет в C#

#![allow(unused)]
fn main() {
// where - ограничения обобщений (гибче, чем where в C#)
fn generic_function<T>() 
where 
    T: Clone + Send + Sync,
{
    // T должен реализовывать трейты Clone, Send и Sync
}

// dyn - динамические трейт-объекты (как object в C#, но с проверкой типов)
let drawable: Box<dyn Draw> = Box::new(Circle::new());

// Self - ссылка на реализующий тип (как this в C#, но для типов)
impl MyStruct {
    fn new() -> Self { // Self = MyStruct
        Self { field: 0 }
    }
}

// self - получатель метода
impl MyStruct {
    fn method(&self) { }        // Неизменяемое заимствование
    fn method_mut(&mut self) { } // Изменяемое заимствование
    fn consume(self) { }        // Принять владение
}

// crate - ссылка на корень текущего крейта
use crate::models::User; // Абсолютный путь от корня крейта

// super - ссылка на родительский модуль
use super::utils; // Импорт из родительского модуля
}

Сводка ключевых слов для разработчиков C#

НазначениеC#RustКлючевое отличие
Видимостьpublic, private, internalpub, по умолчанию приватноеБолее тонкая настройка через pub(crate)
Переменныеvar, readonly, constlet, let mut, constНеизменяемость по умолчанию
Функцииmethod()fnФункции могут существовать самостоятельно
Типыclass, struct, interfacestruct, enum, traitПеречисления — алгебраические типы
Обобщения<T> where T : IFoo<T> where T: FooБолее гибкие ограничения
Ссылкиref, out, in&, &mutПроверка заимствований на этапе компиляции
Паттерныswitch, ismatch, if letТребуется исчерпывающее сопоставление

3. Встроенные типы и переменные

Переменные и изменяемость

Что вы узнаете: модель объявления переменных и изменяемости в Rust в сравнении с var/const в C#, соответствие примитивных типов, важнейшее различие между String и &str, вывод типов, а также то, как Rust по-другому обрабатывает приведение и преобразования типов.

Сложность: 🟢 Начальный

Объявление переменных в C#

// C# — переменные по умолчанию изменяемы
int count = 0;           // Изменяемая
count = 5;               // ✅ Работает

// readonly-поля (только на уровне класса, не для локальных переменных)
// readonly int maxSize = 100;  // Неизменяемо после инициализации

const int BUFFER_SIZE = 1024; // Константа времени компиляции (работает как локальная переменная или поле)

Объявление переменных в Rust

#![allow(unused)]
fn main() {
// Rust — переменные по умолчанию неизменяемы
let count = 0;           // Неизменяемая по умолчанию
// count = 5;            // ❌ Ошибка компиляции: нельзя дважды присвоить неизменяемой переменной

let mut count = 0;       // Явно изменяемая
count = 5;               // ✅ Работает

const BUFFER_SIZE: usize = 1024; // Константа времени компиляции
}

Ключевой сдвиг мышления для разработчиков C#

#![allow(unused)]
fn main() {
// Думайте о 'let' как о семантике readonly-поля, применённой ко всем переменным
let name = "John";       // Как readonly-поле: после присваивания изменить нельзя
let mut age = 30;        // Как: int age = 30;

// Затенение переменных (особенность Rust)
let spaces = "   ";      // Строка
let spaces = spaces.len(); // Теперь это число (usize)
// Это не мутация — мы создаём новую переменную
}

Практический пример: счётчик

// Версия на C#
public class Counter
{
    private int value = 0;
    
    public void Increment()
    {
        value++;  // Мутация
    }
    
    public int GetValue() => value;
}
#![allow(unused)]
fn main() {
// Версия на Rust
pub struct Counter {
    value: i32,  // По умолчанию приватное
}

impl Counter {
    pub fn new() -> Counter {
        Counter { value: 0 }
    }
    
    pub fn increment(&mut self) {  // &mut нужен для мутации
        self.value += 1;
    }
    
    pub fn get_value(&self) -> i32 {
        self.value
    }
}
}

Сравнение типов данных

Примитивные типы

Тип C#Тип RustРазмерДиапазон
byteu88 битот 0 до 255
sbytei88 битот -128 до 127
shorti1616 битот -32 768 до 32 767
ushortu1616 битот 0 до 65 535
inti3232 битаот -2³¹ до 2³¹-1
uintu3232 битаот 0 до 2³²-1
longi6464 битаот -2⁶³ до 2⁶³-1
ulongu6464 битаот 0 до 2⁶⁴-1
floatf3232 битаIEEE 754
doublef6464 битаIEEE 754
boolbool1 битtrue/false
charchar32 битаСкалярное значение Unicode

Типы размера (важно!)

// C# — int всегда 32-битный
int arrayIndex = 0;
long fileSize = file.Length;
#![allow(unused)]
fn main() {
// Rust — типы размера совпадают с размером указателя (32 или 64 бита)
let array_index: usize = 0;    // Как size_t в C
let file_size: u64 = file.len(); // Явно 64-битный
}

Вывод типов

// C# — ключевое слово var
var name = "John";        // string
var count = 42;           // int
var price = 29.99;        // double
#![allow(unused)]
fn main() {
// Rust — автоматический вывод типов
let name = "John";        // &str (срез строки)
let count = 42;           // i32 (целое по умолчанию)
let price = 29.99;        // f64 (вещественное по умолчанию)

// Явные аннотации типов
let count: u32 = 42;
let price: f32 = 29.99;
}

Обзор массивов и коллекций

// C# — ссылочные типы, размещаются в куче
int[] numbers = new int[5];        // Фиксированный размер
List<int> list = new List<int>();  // Динамический размер
#![allow(unused)]
fn main() {
// Rust — несколько вариантов
let numbers: [i32; 5] = [1, 2, 3, 4, 5];  // Массив на стеке, фиксированный размер
let mut list: Vec<i32> = Vec::new();       // Вектор в куче, динамический размер
}

Типы строк: String и &str

Это одна из самых запутанных концепций для разработчиков C#, поэтому разберём её внимательно.

Работа со строками в C#

// C# — простая модель строк
string name = "John";           // Строковый литерал
string greeting = "Hello, " + name;  // Конкатенация строк
string upper = name.ToUpper();  // Вызов метода

Строковые типы Rust

#![allow(unused)]
fn main() {
// Rust — два основных строковых типа

// 1. &str (срез строки) — похож на ReadOnlySpan<char> в C#
let name: &str = "John";        // Строковый литерал (неизменяемый, заимствованный)

// 2. String — похож на StringBuilder или изменяемую строку
let mut greeting = String::new();       // Пустая строка
greeting.push_str("Hello, ");          // Добавление
greeting.push_str(name);               // Добавление

// Или создаём сразу
let greeting = String::from("Hello, John");
let greeting = "Hello, John".to_string();  // Преобразование &str в String
}

Когда что использовать?

СценарийИспользоватьАналог в C#
Строковые литералы&strСтроковый литерал string
Параметры функций (только для чтения)&strstring или ReadOnlySpan<char>
Владеемые, изменяемые строкиStringStringBuilder
Возврат владеемых строкStringstring

Практические примеры

// Функция, принимающая любой строковый тип
fn greet(name: &str) {  // Принимает и String, и &str
    println!("Hello, {}!", name);
}

fn main() {
    let literal = "John";                    // &str
    let owned = String::from("Jane");        // String
    
    greet(literal);                          // Работает
    greet(&owned);                           // Работает (заимствуем String как &str)
    greet("Bob");                            // Работает
}

// Функция, возвращающая владеемую строку
fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)  // Макрос format! возвращает String
}

Разработчикам C# — смотрите так

#![allow(unused)]
fn main() {
// &str — это как ReadOnlySpan<char>: представление данных строки
// String — это как char[], которым вы владеете и которое можете изменять

let borrowed: &str = "Я не владею этими данными";
let owned: String = String::from("Я владею этими данными");

// Преобразования между ними
let owned_copy: String = borrowed.to_string();  // Копия во владение
let borrowed_view: &str = &owned;               // Заимствование из owned
}

Вывод и форматирование строк

Разработчики C# активно используют Console.WriteLine и интерполяцию строк ($""). Система форматирования Rust не менее мощная, но основана на макросах и спецификаторах формата.

Базовый вывод

// Вывод в C#
Console.Write("без перевода строки");
Console.WriteLine("с переводом строки");
Console.Error.WriteLine("в stderr");

// Интерполяция строк (C# 6+)
string name = "Alice";
int age = 30;
Console.WriteLine($"{name} is {age} years old");
#![allow(unused)]
fn main() {
// Вывод в Rust — всё через макросы (обратите внимание на !)
print!("без перевода строки");              // → stdout, без перевода строки
println!("с переводом строки");              // → stdout + перевод строки
eprint!("в stderr");                          // → stderr, без перевода строки
eprintln!("в stderr с переводом строки"); // → stderr + перевод строки

// Форматирование строк (аналог интерполяции $"")
let name = "Alice";
let age = 30;
println!("{name} is {age} years old");     // Захват переменной прямо в строке (Rust 1.58+)
println!("{} is {} years old", name, age); // Позиционные аргументы

// format! возвращает String вместо вывода
let msg = format!("{name} is {age} years old");
}

Спецификаторы формата

// Спецификаторы формата в C#
Console.WriteLine($"{price:F2}");         // Фиксированная запись: 29.99
Console.WriteLine($"{count:D5}");         // Дополнение нулями: 00042
Console.WriteLine($"{value,10}");         // Выравнивание по правому краю, ширина 10
Console.WriteLine($"{value,-10}");        // Выравнивание по левому краю, ширина 10
Console.WriteLine($"{hex:X}");            // Шестнадцатеричный: FF
Console.WriteLine($"{ratio:P1}");         // Проценты: 85.0%
#![allow(unused)]
fn main() {
// Спецификаторы формата в Rust
println!("{price:.2}");          // 2 знака после запятой: 29.99
println!("{count:05}");          // Дополнение нулями, ширина 5: 00042
println!("{value:>10}");         // Выравнивание по правому краю, ширина 10
println!("{value:<10}");         // Выравнивание по левому краю, ширина 10
println!("{value:^10}");         // Выравнивание по центру, ширина 10
println!("{hex:#X}");            // Шестнадцатеричный с префиксом: 0xFF
println!("{hex:08X}");           // Шестнадцатеричный с нулями: 000000FF
println!("{bits:#010b}");        // Двоичный с префиксом: 0b00001010
println!("{big}", big = 1_000_000); // Именованный параметр
}

Вывод Debug и Display

#![allow(unused)]
fn main() {
// {:?}  — трейт Debug (для разработчиков, реализуется автоматически)
// {:#?} — Debug с красивым форматированием (с отступами, в несколько строк)
// {}    — трейт Display (для пользователей, нужно реализовать вручную)

#[derive(Debug)] // Автоматически генерирует вывод Debug
struct Point { x: f64, y: f64 }

let p = Point { x: 1.5, y: 2.7 };

println!("{:?}", p);   // Point { x: 1.5, y: 2.7 }   — компактный debug
println!("{:#?}", p);  // Point {                     — debug с форматированием
                        //     x: 1.5,
                        //     y: 2.7,
                        // }
// println!("{}", p);  // ❌ ОШИБКА: Point не реализует Display

// Реализуем Display для вывода, понятного пользователю:
use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
println!("{}", p);    // (1.5, 2.7)  — удобно для пользователя
}
// Эквивалент в C#:
// {:?}  ≈ object.GetType().ToString() или дамп через рефлексию
// {}    ≈ object.ToString()
// В C# вы переопределяете ToString(); в Rust вы реализуете Display

Краткая справка

C#RustВывод
Console.WriteLine(x)println!("{x}")Форматирование Display
$"{x}" (интерполяция)format!("{x}")Возвращает String
x.ToString()x.to_string()Требует трейт Display
Переопределение ToString()impl DisplayВывод для пользователя
Просмотр в отладчике{:?} или dbg!(x)Вывод для разработчика
String.Format("{0:F2}", x)format!("{x:.2}")Форматированная String
Console.Error.WriteLineeprintln!()Запись в stderr

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

В C# есть неявные преобразования, явные приведения (int)x и Convert.To*(). Rust строже — неявных числовых преобразований здесь нет.

Числовые преобразования

// C# — неявные и явные преобразования
int small = 42;
long big = small;              // Неявное расширение: OK
double d = small;              // Неявное расширение: OK
int truncated = (int)3.14;     // Явное сужение: 3
byte b = (byte)300;            // Тихое переполнение: 44

// Безопасное преобразование
if (int.TryParse("42", out int parsed)) { /* ... */ }
#![allow(unused)]
fn main() {
// Rust — ВСЕ числовые преобразования явные
let small: i32 = 42;
let big: i64 = small as i64;       // Расширение: явно через 'as'
let d: f64 = small as f64;         // Целое в вещественное: явно
let truncated: i32 = 3.14_f64 as i32; // Сужение: 3 (отбрасывается дробная часть)
let b: u8 = 300_u16 as u8;        // Переполнение: оборачивается до 44 (как unchecked в C#)

// Безопасное преобразование через TryFrom
use std::convert::TryFrom;
let safe: Result<u8, _> = u8::try_from(300_u16); // Err — вне диапазона
let ok: Result<u8, _>   = u8::try_from(42_u16);  // Ok(42)

// Разбор строки — возвращает Result, а не bool + out-параметр
let parsed: Result<i32, _> = "42".parse::<i32>();   // Ok(42)
let bad: Result<i32, _>    = "abc".parse::<i32>();  // Err(ParseIntError)

// С синтаксисом turbofish:
let n = "42".parse::<f64>().unwrap(); // 42.0
}

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

// C#
int n = 42;
string s = n.ToString();          // "42"
string formatted = $"{n:X}";
int back = int.Parse(s);          // 42 или исключение
bool ok = int.TryParse(s, out int result);
#![allow(unused)]
fn main() {
// Rust — to_string() через Display, parse() через FromStr
let n: i32 = 42;
let s: String = n.to_string();            // "42" (использует трейт Display)
let formatted = format!("{n:X}");         // "2A"
let back: i32 = s.parse().unwrap();       // 42 или panic
let result: Result<i32, _> = s.parse();   // Ok(42) — безопасная версия

// Преобразования &str ↔ String (самое частое преобразование в Rust)
let owned: String = "hello".to_string();    // &str → String
let owned2: String = String::from("hello"); // &str → String (то же самое)
let borrowed: &str = &owned;               // String → &str (бесплатно, это просто заимствование)
}

Преобразования ссылочных типов (никакого приведения по наследованию!)

// C# — приведение вверх и вниз по иерархии
Animal a = new Dog();              // Приведение вверх (неявное)
Dog d = (Dog)a;                    // Приведение вниз (явное, может бросить исключение)
if (a is Dog dog) { /* ... */ }    // Безопасное приведение вниз через сопоставление с образцом
#![allow(unused)]
fn main() {
// Rust — нет наследования, нет приведения вверх и вниз
// Для полиморфизма используйте трейт-объекты:
let animal: Box<dyn Animal> = Box::new(Dog);

// «Приведение вниз» требует трейта Any (нужно редко):
use std::any::Any;
if let Some(dog) = animal_any.downcast_ref::<Dog>() {
    // Используем dog
}
// На практике вместо приведения вниз используйте перечисления:
enum Animal {
    Dog(Dog),
    Cat(Cat),
}
match animal {
    Animal::Dog(d) => { /* используем d */ }
    Animal::Cat(c) => { /* используем c */ }
}
}

Краткая справка

C#RustПримечания
(int)xx as i32Усечение/оборачивание при приведении
Неявное расширениеНужно использовать asНеявных числовых преобразований нет
Convert.ToInt32(x)i32::try_from(x)Безопасно, возвращает Result
int.Parse(s)s.parse::<i32>().unwrap()Паника при ошибке
int.TryParse(s, out n)s.parse::<i32>()Возвращает Result<i32, _>
(Dog)animalНедоступноИспользуйте перечисления или Any
as Dog / is Dogdowncast_ref::<Dog>()Через трейт Any; предпочтительны перечисления

Комментарии и документация

Обычные комментарии

// Комментарии в C#
// Однострочный комментарий
/* Многострочный
   комментарий */

/// <summary>
/// XML-комментарий документации
/// </summary>
/// <param name="name">Имя пользователя</param>
/// <returns>Строка приветствия</returns>
public string Greet(string name)
{
    return $"Hello, {name}!";
}
#![allow(unused)]
fn main() {
// Комментарии в Rust
// Однострочный комментарий
/* Многострочный
   комментарий */

/// Комментарий документации (аналог C# ///)
/// Эта функция приветствует пользователя по имени.
/// 
/// # Аргументы
/// 
/// * `name` - Имя пользователя в виде среза строки
/// 
/// # Возвращает
/// 
/// `String`, содержащую приветствие
/// 
/// # Примеры
/// 
/// ```
/// let greeting = greet("Alice");
/// assert_eq!(greeting, "Hello, Alice!");
/// ```
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
}

Генерация документации

# Генерация документации (аналог XML-документации в C#)
cargo doc --open

# Запуск тестов документации
cargo test --doc

Упражнения

🏋️ Упражнение: температура с безопасными типами (нажмите, чтобы раскрыть)

Напишите программу на Rust, которая:

  1. Объявляет const для абсолютного нуля в градусах Цельсия (-273.15)
  2. Объявляет static-счётчик количества выполненных преобразований (используйте AtomicU32)
  3. Пишет функцию celsius_to_fahrenheit(c: f64) -> f64, которая отвергает температуры ниже абсолютного нуля, возвращая f64::NAN
  4. Демонстрирует затенение: разбирает строку "98.6" в f64, а затем преобразует её
🔑 Решение
use std::sync::atomic::{AtomicU32, Ordering};

const ABSOLUTE_ZERO_C: f64 = -273.15;
static CONVERSION_COUNT: AtomicU32 = AtomicU32::new(0);

fn celsius_to_fahrenheit(c: f64) -> f64 {
    if c < ABSOLUTE_ZERO_C {
        return f64::NAN;
    }
    CONVERSION_COUNT.fetch_add(1, Ordering::Relaxed);
    c * 9.0 / 5.0 + 32.0
}

fn main() {
    let temp = "98.6";           // &str
    let temp: f64 = temp.parse().unwrap(); // затеняем как f64
    let temp = celsius_to_fahrenheit(temp); // затеняем как Фаренгейт
    println!("{temp:.1}°F");
    println!("Преобразований: {}", CONVERSION_COUNT.load(Ordering::Relaxed));
}

Истинная неизменяемость и иллюзии record

Истинная неизменяемость против иллюзий record

Что вы узнаете: почему типы record в C# не являются по-настоящему неизменяемыми (изменяемые поля, обход через рефлексию), как Rust обеспечивает настоящую неизменяемость на этапе компиляции и когда использовать паттерны внутренней изменяемости.

Сложность: 🟡 Средний

Records в C# — театр неизменяемости

// Records в C# выглядят неизменяемыми, но у них есть лазейки
public record Person(string Name, int Age, List<string> Hobbies);

var person = new Person("John", 30, new List<string> { "reading" });

// Все эти операции «выглядят» как создание новых экземпляров:
var older = person with { Age = 31 };  // Новый record
var renamed = person with { Name = "Jonathan" };  // Новый record

// Но ссылочные типы всё ещё изменяемы!
person.Hobbies.Add("gaming");  // Меняет исходный объект!
Console.WriteLine(older.Hobbies.Count);  // 2 — older тоже затронут!
Console.WriteLine(renamed.Hobbies.Count); // 2 — renamed тоже затронут!

// Свойства с init-доступом всё равно можно установить через рефлексию
typeof(Person).GetProperty("Age")?.SetValue(person, 25);

// Коллекционные выражения помогают, но не решают фундаментальную проблему
public record BetterPerson(string Name, int Age, IReadOnlyList<string> Hobbies);

var betterPerson = new BetterPerson("Jane", 25, new List<string> { "painting" });
// Всё равно изменяемо через приведение типов:
((List<string>)betterPerson.Hobbies).Add("hacking the system");

// Даже «неизменяемые» коллекции не являются по-настоящему неизменяемыми
using System.Collections.Immutable;
public record SafePerson(string Name, int Age, ImmutableList<string> Hobbies);
// Это лучше, но требует дисциплины и имеет накладные расходы на производительность

Rust — настоящая неизменяемость по умолчанию

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct Person {
    name: String,
    age: u32,
    hobbies: Vec<String>,
}

let person = Person {
    name: "John".to_string(),
    age: 30,
    hobbies: vec!["reading".to_string()],
};

// Этот код просто не скомпилируется:
// person.age = 31;  // ОШИБКА: нельзя присвоить значение неизменяемому полю
// person.hobbies.push("gaming".to_string());  // ОШИБКА: нельзя заимствовать как изменяемое

// Для изменения нужно явно включить 'mut':
let mut older_person = person.clone();
older_person.age = 31;  // Теперь ясно, что это мутация

// Или используйте паттерн обновления через структурное выражение:
let renamed = Person {
    name: "Jonathan".to_string(),
    ..person  // Копирует остальные поля (применяется семантика перемещения)
};

// Исходный объект гарантированно не меняется (пока не перемещён):
println!("{:?}", person.hobbies);  // Всегда ["reading"] — неизменяемо

// Структурное разделение с эффективными неизменяемыми структурами данных
use std::rc::Rc;

#[derive(Debug, Clone)]
struct EfficientPerson {
    name: String,
    age: u32,
    hobbies: Rc<Vec<String>>,  // Общая, неизменяемая ссылка
}

// Создание новых версий эффективно разделяет данные
let person1 = EfficientPerson {
    name: "Alice".to_string(),
    age: 30,
    hobbies: Rc::new(vec!["reading".to_string(), "cycling".to_string()]),
};

let person2 = EfficientPerson {
    name: "Bob".to_string(),
    age: 25,
    hobbies: Rc::clone(&person1.hobbies),  // Общая ссылка, без глубокого копирования
};
}
graph TD
    subgraph "Records C# — поверхностная неизменяемость"
        CS_RECORD["record Person(...)"]
        CS_WITH["выражения with"]
        CS_SHALLOW["⚠️ Неизменяем только верхний уровень"]
        CS_REF_MUT["❌ Ссылочные типы всё ещё изменяемы"]
        CS_REFLECTION["❌ Рефлексия может обойти ограничения"]
        CS_RUNTIME["❌ Сюрпризы во время выполнения"]
        CS_DISCIPLINE["😓 Требуется дисциплина команды"]
        
        CS_RECORD --> CS_WITH
        CS_WITH --> CS_SHALLOW
        CS_SHALLOW --> CS_REF_MUT
        CS_RECORD --> CS_REFLECTION
        CS_REF_MUT --> CS_RUNTIME
        CS_RUNTIME --> CS_DISCIPLINE
    end
    
    subgraph "Rust — настоящая неизменяемость"
        RUST_STRUCT["struct Person { ... }"]
        RUST_DEFAULT["✅ Неизменяемо по умолчанию"]
        RUST_COMPILE["✅ Принудительная проверка на этапе компиляции"]
        RUST_MUT["🔒 Требуется явный 'mut'"]
        RUST_MOVE["🔄 Семантика перемещения"]
        RUST_ZERO["⚡ Нулевые накладные расходы рантайма"]
        RUST_SAFE["🛡️ Безопасность памяти"]
        
        RUST_STRUCT --> RUST_DEFAULT
        RUST_DEFAULT --> RUST_COMPILE
        RUST_COMPILE --> RUST_MUT
        RUST_MUT --> RUST_MOVE
        RUST_MOVE --> RUST_ZERO
        RUST_ZERO --> RUST_SAFE
    end
    
    style CS_REF_MUT fill:#ffcdd2,color:#000
    style CS_REFLECTION fill:#ffcdd2,color:#000
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style RUST_COMPILE fill:#c8e6c9,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000

Упражнения

🏋️ Упражнение: докажите неизменяемость (нажмите, чтобы раскрыть)

Ваш коллега на C# утверждает, что его record неизменяем. Переведите этот код на Rust и объясните, почему версия на Rust действительно неизменяема:

public record Config(string Host, int Port, List<string> AllowedOrigins);

var config = new Config("localhost", 8080, new List<string> { "example.com" });
// «Неизменяемый» record... но:
config.AllowedOrigins.Add("evil.com"); // Компилируется! List изменяем.
  1. Создайте эквивалентную структуру на Rust, которая действительно неизменяема
  2. Покажите, что попытка изменить allowed_origins — это ошибка компиляции
  3. Напишите функцию, которая создаёт изменённую копию (с новым хостом) без мутации
🔑 Решение
#[derive(Debug, Clone)]
struct Config {
    host: String,
    port: u16,
    allowed_origins: Vec<String>,
}

impl Config {
    fn with_host(&self, host: impl Into<String>) -> Self {
        Config {
            host: host.into(),
            ..self.clone()
        }
    }
}

fn main() {
    let config = Config {
        host: "localhost".into(),
        port: 8080,
        allowed_origins: vec!["example.com".into()],
    };

    // config.allowed_origins.push("evil.com".into());
    // ❌ ОШИБКА: нельзя заимствовать `config.allowed_origins` как изменяемое

    let production = config.with_host("prod.example.com");
    println!("Dev: {:?}", config);       // исходный объект не изменился
    println!("Prod: {:?}", production);  // новая копия с другим хостом
}

Ключевая мысль: в Rust let config = ... (без mut) делает всё дерево значений неизменяемым — включая вложенный Vec. Records в C# делают неизменяемой только ссылку, но не содержимое.


4. Управление потоком выполнения

Функции и методы

Что вы узнаете: функции и методы в Rust по сравнению с C#, важнейшее различие между выражениями и инструкциями, синтаксис if/match/loop/while/for, а также то, как ориентированный на выражения дизайн Rust делает ненужными тернарные операторы.

Сложность: 🟢 Начальный

Объявление функций в C#

// C# — методы внутри классов
public class Calculator
{
    // Метод экземпляра
    public int Add(int a, int b)
    {
        return a + b;
    }
    
    // Статический метод
    public static int Multiply(int a, int b)
    {
        return a * b;
    }
    
    // Метод с параметром ref
    public void Increment(ref int value)
    {
        value++;
    }
}

Объявление функций в Rust

// Rust — самостоятельные функции
fn add(a: i32, b: i32) -> i32 {
    a + b  // Ключевое слово 'return' не нужно для последнего выражения
}

fn multiply(a: i32, b: i32) -> i32 {
    return a * b;  // Явный return тоже допустим
}

// Функция с изменяемой ссылкой
fn increment(value: &mut i32) {
    *value += 1;
}

fn main() {
    let result = add(5, 3);
    println!("5 + 3 = {}", result);
    
    let mut x = 10;
    increment(&mut x);
    println!("После increment: {}", x);
}

Выражение против инструкции (важно!)

graph LR
    subgraph "C# — инструкции"
        CS1["if (cond)"] --> CS2["return 42;"]
        CS1 --> CS3["return 0;"]
        CS2 --> CS4["Значение выходит через return"]
        CS3 --> CS4
    end
    subgraph "Rust — выражения"
        RS1["if cond"] --> RS2["42  (без точки с запятой)"]
        RS1 --> RS3["0  (без точки с запятой)"]
        RS2 --> RS4["Блок И ЕСТЬ значение"]
        RS3 --> RS4
    end

    style CS4 fill:#bbdefb,color:#000
    style RS4 fill:#c8e6c9,color:#000
// C# — инструкции и выражения
public int GetValue()
{
    if (condition)
    {
        return 42;  // Инструкция
    }
    return 0;       // Инструкция
}
#![allow(unused)]
fn main() {
// Rust — почти всё может быть выражением
fn get_value(condition: bool) -> i32 {
    if condition {
        42  // Выражение (без точки с запятой)
    } else {
        0   // Выражение (без точки с запятой)
    }
    // Сам блок if-else — это выражение, которое возвращает значение
}

// Или ещё проще
fn get_value_ternary(condition: bool) -> i32 {
    if condition { 42 } else { 0 }
}
}

Параметры функций и типы возвращаемых значений

// Без параметров и без возвращаемого значения (возвращает тип unit ())
fn say_hello() {
    println!("Привет!");
}

// Несколько параметров
fn greet(name: &str, age: u32) {
    println!("{} — {} лет", name, age);
}

// Несколько возвращаемых значений через кортеж
fn divide_and_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
    (dividend / divisor, dividend % divisor)
}

fn main() {
    let (quotient, remainder) = divide_and_remainder(10, 3);
    println!("10 ÷ 3 = {} остаток {}", quotient, remainder);
}

Основы управления потоком

Условные инструкции

// Инструкции if в C#
int x = 5;
if (x > 10)
{
    Console.WriteLine("Большое число");
}
else if (x > 5)
{
    Console.WriteLine("Среднее число");
}
else
{
    Console.WriteLine("Маленькое число");
}

// Тернарный оператор C#
string message = x > 10 ? "Большое" : "Маленькое";
#![allow(unused)]
fn main() {
// Выражения if в Rust
let x = 5;
if x > 10 {
    println!("Большое число");
} else if x > 5 {
    println!("Среднее число");
} else {
    println!("Маленькое число");
}

// if как выражение (аналог тернарного оператора)
let message = if x > 10 { "Большое" } else { "Маленькое" };

// Несколько условий
let message = if x > 10 {
    "Большое"
} else if x > 5 {
    "Среднее"
} else {
    "Маленькое"
};
}

Циклы

// Циклы в C#
// Цикл for
for (int i = 0; i < 5; i++)
{
    Console.WriteLine(i);
}

// Цикл foreach
var numbers = new[] { 1, 2, 3, 4, 5 };
foreach (var num in numbers)
{
    Console.WriteLine(num);
}

// Цикл while
int count = 0;
while (count < 3)
{
    Console.WriteLine(count);
    count++;
}
#![allow(unused)]
fn main() {
// Циклы в Rust
// Цикл по диапазону
for i in 0..5 {  // От 0 до 4 (правая граница не включается)
    println!("{}", i);
}

// Перебор коллекции
let numbers = vec![1, 2, 3, 4, 5];
for num in numbers {  // Забирает владение
    println!("{}", num);
}

// Перебор ссылок (встречается чаще)
let numbers = vec![1, 2, 3, 4, 5];
for num in &numbers {  // Заимствует элементы
    println!("{}", num);
}

// Цикл while
let mut count = 0;
while count < 3 {
    println!("{}", count);
    count += 1;
}

// Бесконечный цикл с break
let mut counter = 0;
loop {
    if counter >= 3 {
        break;
    }
    println!("{}", counter);
    counter += 1;
}
}

Управление циклом

// Управление циклом в C#
for (int i = 0; i < 10; i++)
{
    if (i == 3) continue;
    if (i == 7) break;
    Console.WriteLine(i);
}
#![allow(unused)]
fn main() {
// Управление циклом в Rust
for i in 0..10 {
    if i == 3 { continue; }
    if i == 7 { break; }
    println!("{}", i);
}

// Метки циклов (для вложенных циклов)
'outer: for i in 0..3 {
    'inner: for j in 0..3 {
        if i == 1 && j == 1 {
            break 'outer;  // Выход из внешнего цикла
        }
        println!("i: {}, j: {}", i, j);
    }
}
}

🏋️ Упражнение: конвертер температур (нажмите, чтобы раскрыть)

Задача: переведите эту программу на C# на идиоматичный Rust. Используйте выражения, сопоставление с образцом и правильную обработку ошибок.

// C# — переведите это на Rust
public static double Convert(double value, string from, string to)
{
    double celsius = from switch
    {
        "F" => (value - 32.0) * 5.0 / 9.0,
        "K" => value - 273.15,
        "C" => value,
        _ => throw new ArgumentException($"Unknown unit: {from}")
    };
    return to switch
    {
        "F" => celsius * 9.0 / 5.0 + 32.0,
        "K" => celsius + 273.15,
        "C" => celsius,
        _ => throw new ArgumentException($"Unknown unit: {to}")
    };
}
🔑 Решение
#[derive(Debug, Clone, Copy)]
enum TempUnit { Celsius, Fahrenheit, Kelvin }

fn parse_unit(s: &str) -> Result<TempUnit, String> {
    match s {
        "C" => Ok(TempUnit::Celsius),
        "F" => Ok(TempUnit::Fahrenheit),
        "K" => Ok(TempUnit::Kelvin),
        _   => Err(format!("Неизвестная единица: {s}")),
    }
}

fn convert(value: f64, from: TempUnit, to: TempUnit) -> f64 {
    let celsius = match from {
        TempUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0,
        TempUnit::Kelvin     => value - 273.15,
        TempUnit::Celsius    => value,
    };
    match to {
        TempUnit::Fahrenheit => celsius * 9.0 / 5.0 + 32.0,
        TempUnit::Kelvin     => celsius + 273.15,
        TempUnit::Celsius    => celsius,
    }
}

fn main() -> Result<(), String> {
    let from = parse_unit("F")?;
    let to   = parse_unit("C")?;
    println!("212°F = {:.1}°C", convert(212.0, from, to));
    Ok(())
}

Ключевые выводы:

  • Перечисления заменяют «магические» строки — исчерпывающее сопоставление ловит пропущенные единицы на этапе компиляции
  • Result<T, E> заменяет исключения — вызывающий код видит возможные ошибки в сигнатуре
  • match — это выражение, которое возвращает значение, — инструкции return не нужны

5. Структуры данных и коллекции

Кортежи и деструктуризация

Что вы узнаете: кортежи Rust против ValueTuple в C#, массивы и срезы, структуры против классов, паттерн newtype для моделирования предметной области с безопасностью типов без накладных расходов, а также синтаксис деструктуризации.

Сложность: 🟢 Начальный

В C# есть ValueTuple (начиная с C# 7). Кортежи Rust похожи на них, но более глубоко интегрированы в язык.

Кортежи в C#

// ValueTuple в C# (C# 7+)
var point = (10, 20);                         // (int, int)
var named = (X: 10, Y: 20);                   // Именованные элементы
Console.WriteLine($"{named.X}, {named.Y}");

// Кортеж как возвращаемый тип
public (int Quotient, int Remainder) Divide(int a, int b)
{
    return (a / b, a % b);
}

var (q, r) = Divide(10, 3);    // Деконструкция
Console.WriteLine($"{q} remainder {r}");

// Пропуски (discards)
var (_, remainder) = Divide(10, 3);  // Игнорируем частное

Кортежи в Rust

#![allow(unused)]
fn main() {
// Кортежи Rust — неизменяемы по умолчанию, без именованных элементов
let point = (10, 20);                // (i32, i32)
let point3d: (f64, f64, f64) = (1.0, 2.0, 3.0);

// Доступ по индексу (с нуля)
println!("x={}, y={}", point.0, point.1);

// Кортеж как возвращаемый тип
fn divide(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}

let (q, r) = divide(10, 3);       // Деструктуризация
println!("{q} remainder {r}");

// Пропуск через _
let (_, remainder) = divide(10, 3);

// Тип unit () — «пустой кортеж» (аналог void в C#)
fn greet() {          // неявный возвращаемый тип — ()
    println!("hi");
}
}

Ключевые различия

ХарактеристикаValueTuple в C#Кортеж Rust
Именованные элементы(int X, int Y)Не поддерживаются — используйте структуры
Максимальная арность~8 (для большего — вложенность)Без ограничений (практически ~12)
СравнениеАвтоматическиАвтоматически для кортежей до 12 элементов
Использование как ключ словаряДаДа (если элементы реализуют Hash)
Возврат из функцийЧастоЧасто
Изменяемые элементыВсегда изменяемыТолько при let mut

Кортежные структуры (newtype)

#![allow(unused)]
fn main() {
// Когда обычного кортежа недостаточно, используйте кортежную структуру:
struct Meters(f64);     // Обёртка «newtype» с одним полем
struct Celsius(f64);
struct Fahrenheit(f64);

// Компилятор считает их РАЗНЫМИ типами:
let distance = Meters(100.0);
let temp = Celsius(36.6);
// distance == temp;  // ❌ ОШИБКА: нельзя сравнивать Meters и Celsius

// Паттерн newtype предотвращает ошибки перепутанных единиц на этапе компиляции!
// В C# для той же безопасности понадобился бы полноценный класс или структура.
}
// В C# аналог требует больше церемоний:
public readonly record struct Meters(double Value);
public readonly record struct Celsius(double Value);
// Они не взаимозаменяемы, но records добавляют накладные расходы по сравнению с newtype в Rust, которые не несут затрат

Паттерн newtype в деталях: моделирование предметной области без затрат

Newtype’ы гораздо полезнее, чем защита от путаницы единиц измерения. Это основной инструмент Rust для кодирования бизнес-правил в системе типов — он заменяет шаблоны «защитного условия» и «класса валидации», распространённые в C#.

Подход C# к валидации: проверки во время выполнения

// C# — валидация происходит во время выполнения, каждый раз
public class UserService
{
    public User CreateUser(string email, int age)
    {
        if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
            throw new ArgumentException("Invalid email");
        if (age < 0 || age > 150)
            throw new ArgumentException("Invalid age");

        return new User { Email = email, Age = age };
    }

    public void SendEmail(string email)
    {
        // Нужно перепроверить — или доверять вызывающему коду?
        if (!email.Contains('@')) throw new ArgumentException("Invalid email");
        // ...
    }
}

Подход Rust с newtype: доказательство на этапе компиляции

#![allow(unused)]
fn main() {
/// Проверенный адрес электронной почты — сам тип является доказательством корректности.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String);

impl Email {
    /// ЕДИНСТВЕННЫЙ способ создать Email — валидация выполняется один раз, при создании.
    pub fn new(raw: &str) -> Result<Self, &'static str> {
        if raw.contains('@') && raw.len() > 3 {
            Ok(Email(raw.to_lowercase()))
        } else {
            Err("invalid email format")
        }
    }

    /// Безопасный доступ к внутреннему значению
    pub fn as_str(&self) -> &str { &self.0 }
}

/// Проверенный возраст — невозможно создать некорректный.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Age(u8);

impl Age {
    pub fn new(raw: u8) -> Result<Self, &'static str> {
        if raw <= 150 { Ok(Age(raw)) } else { Err("age out of range") }
    }
    pub fn value(&self) -> u8 { self.0 }
}

// Теперь функции принимают ДОКАЗАННЫЕ типы — повторная валидация не нужна!
fn create_user(email: Email, age: Age) -> User {
    // email ГАРАНТИРОВАННО корректен — это инвариант типа
    User { email, age }
}

fn send_email(to: &Email) {
    // Валидация не нужна — тип Email доказывает корректность
    println!("Sending to: {}", to.as_str());
}
}

Типичные применения newtype для разработчиков C#

Паттерн в C#Newtype в RustЧто предотвращает
string для UserId, Email и т. д.struct UserId(Uuid)Передачу неверной строки в неверный параметр
int для Port, Count, Indexstruct Port(u16)Port и Count нельзя перепутать
Защитные условия повсюдуОднократная валидация в конструктореПовторную проверку, пропущенную валидацию
decimal для USD, EURstruct Usd(Decimal)Случайное сложение USD с EUR
TimeSpan с разной семантикойstruct Timeout(Duration)Передачу таймаута соединения как таймаута запроса
#![allow(unused)]
fn main() {
// Без затрат: newtype компилируется в тот же машинный код, что и внутренний тип.
// Этот код на Rust:
struct UserId(u64);
fn lookup(id: UserId) -> Option<User> { /* ... */ }

// Генерирует ТОТ ЖЕ машинный код, что и:
fn lookup(id: u64) -> Option<User> { /* ... */ }
// Но с полной безопасностью типов на этапе компиляции!
}

Массивы и срезы

Понимание различий между массивами, срезами и векторами крайне важно.

Массивы в C#

// Массивы в C#
int[] numbers = new int[5];         // Фиксированный размер, размещается в куче
int[] initialized = { 1, 2, 3, 4, 5 }; // Литерал массива

// Доступ
numbers[0] = 10;
int first = numbers[0];

// Длина
int length = numbers.Length;

// Массив как параметр (ссылочный тип)
void ProcessArray(int[] array)
{
    array[0] = 99;  // Изменяет исходный массив
}

Массивы, срезы и векторы в Rust

#![allow(unused)]
fn main() {
// 1. Массивы — фиксированный размер, размещаются на стеке
let numbers: [i32; 5] = [1, 2, 3, 4, 5];  // Тип: [i32; 5]
let zeros = [0; 10];                       // 10 нулей

// Доступ
let first = numbers[0];
// numbers[0] = 10;  // ❌ Ошибка: массивы по умолчанию неизменяемы

let mut mut_array = [1, 2, 3, 4, 5];
mut_array[0] = 10;  // ✅ Работает с mut

// 2. Срезы — представления массивов или векторов
let slice: &[i32] = &numbers[1..4];  // Элементы 1, 2, 3
let all_slice: &[i32] = &numbers;    // Весь массив как срез

// 3. Векторы — динамический размер, размещаются в куче (рассмотрены ранее)
let mut vec = vec![1, 2, 3, 4, 5];
vec.push(6);  // Может расти
}

Срезы как параметры функций

// C# — метод, работающий с массивами
public void ProcessNumbers(int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        Console.WriteLine(numbers[i]);
    }
}

// Работает только с массивами
ProcessNumbers(new int[] { 1, 2, 3 });
// Rust — функция, работающая с любой последовательностью
fn process_numbers(numbers: &[i32]) {  // Параметр-срез
    for (i, num) in numbers.iter().enumerate() {
        println!("Индекс {}: {}", i, num);
    }
}

fn main() {
    let array = [1, 2, 3, 4, 5];
    let vec = vec![1, 2, 3, 4, 5];
    
    // Одна и та же функция работает с обоими!
    process_numbers(&array);      // Массив как срез
    process_numbers(&vec);        // Вектор как срез
    process_numbers(&vec[1..4]);  // Частичный срез
}

Строковые срезы (&str) — повторно

#![allow(unused)]
fn main() {
// Связь между String и &str
fn string_slice_example() {
    let owned = String::from("Hello, World!");
    let slice: &str = &owned[0..5];      // "Hello"
    let slice2: &str = &owned[7..];      // "World!"
    
    println!("{}", slice);   // "Hello"
    println!("{}", slice2);  // "World!"
    
    // Функция, принимающая любой строковый тип
    print_string("String literal");      // &str
    print_string(&owned);               // String как &str
    print_string(slice);                // Срез &str
}

fn print_string(s: &str) {
    println!("{}", s);
}
}

Современный C#: Span<T> и встроенные массивы

C# ушёл далеко от традиционных массивов. Span<T> предоставляет типобезопасные представления непрерывной памяти, которые могут находиться на стеке, а встроенные массивы (C# 12) дают буферы фиксированного размера на стеке.

// Span<T> в C# — представление непрерывной памяти
Span<int> span = stackalloc int[] { 1, 2, 3, 4, 5 };
span[0] = 10;

ReadOnlySpan<char> text = "Hello".AsSpan();

// Метод, принимающий любое представление непрерывной памяти
void ProcessSpan(ReadOnlySpan<int> data)
{
    for (int i = 0; i < data.Length; i++)
        Console.WriteLine(data[i]);
}

// Встроенные массивы (C# 12) — буфер фиксированного размера на стеке
[InlineArray(5)]
struct IntBuffer
{
    private int _element;
}
#![allow(unused)]
fn main() {
// Rust &[T] / &mut [T] — заимствованное представление непрерывной памяти
let mut array = [1, 2, 3, 4, 5];
let slice: &mut [i32] = &mut array;
slice[0] = 10;

let slice: &[i32] = &array;
let text: &str = "Hello";

// Функция, принимающая любые последовательные данные
fn process_slice(data: &[i32]) {
    for (i, num) in data.iter().enumerate() {
        println!("Индекс {}: {}", i, num);
    }
}

// Массивы фиксированного размера (размещаются на стеке)
let buffer: [i32; 5] = [0; 5];
}
C#Rust
Span<T> (ref struct, только на стеке)&mut [T] / &[T] (заимствованный срез)
ReadOnlySpan<T>&[T] (неизменяемый срез)
ReadOnlySpan<char> / string.AsSpan()&str (строковый срез)
Структура [InlineArray(N)] (C# 12)[T; N] (массив фиксированного размера)
stackalloc T[] с Span<T>let arr: [T; N] = ... (локальный массив)

Ключевая мысль: &[T] в Rust совмещает роли ArraySegment<T>, Span<T> и ReadOnlySpan<T> из C# — это «толстый» указатель (указатель + длина), который работает с массивами, векторами и подсрезами. Встроенные массивы C# естественно соответствуют массивам [T; N] в Rust, которые тоже по умолчанию размещаются на стеке.


Структуры против классов

Структуры в Rust похожи на классы C#, но имеют несколько ключевых отличий, связанных с владением и методами.

graph TD
    subgraph "Класс C# (куча)"
        CObj["Заголовок объекта<br/>+ указатель на vtable"] --> CFields["Name: ссылка на string<br/>Age: int<br/>Hobbies: ссылка на List"]
        CFields --> CHeap1["#quot;Alice#quot; в куче"]
        CFields --> CHeap2["List&lt;string&gt; в куче"]
    end
    subgraph "Структура Rust (стек)"
        RFields["name: String<br/>  ptr | len | cap<br/>age: i32<br/>hobbies: Vec<br/>  ptr | len | cap"]
        RFields --> RHeap1["#quot;Alice#quot; буфер в куче"]
        RFields --> RHeap2["Буфер Vec в куче"]
    end

    style CObj fill:#bbdefb,color:#000
    style RFields fill:#c8e6c9,color:#000

Ключевая мысль: классы C# всегда живут в куче за ссылкой. Структуры Rust по умолчанию живут на стеке — в куче находятся только данные динамического размера (например, содержимое String). Это устраняет накладные расходы GC для небольших часто создаваемых объектов.

Определение класса в C#

// Класс C# со свойствами и методами
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<string> Hobbies { get; set; }
    
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
        Hobbies = new List<string>();
    }
    
    public void AddHobby(string hobby)
    {
        Hobbies.Add(hobby);
    }
    
    public string GetInfo()
    {
        return $"{Name} is {Age} years old";
    }
}

Определение структуры в Rust

#![allow(unused)]
fn main() {
// Структура Rust с ассоциированными функциями и методами
#[derive(Debug)]  // Автоматически реализует трейт Debug
pub struct Person {
    pub name: String,    // Публичное поле
    pub age: u32,        // Публичное поле
    hobbies: Vec<String>, // Приватное поле (без pub)
}

impl Person {
    // Ассоциированная функция (аналог статического метода)
    pub fn new(name: String, age: u32) -> Person {
        Person {
            name,
            age,
            hobbies: Vec::new(),
        }
    }
    
    // Метод (принимает &self, &mut self или self)
    pub fn add_hobby(&mut self, hobby: String) {
        self.hobbies.push(hobby);
    }
    
    // Метод, который заимствует неизменяемо
    pub fn get_info(&self) -> String {
        format!("{} is {} years old", self.name, self.age)
    }
    
    // Геттер для приватного поля
    pub fn hobbies(&self) -> &Vec<String> {
        &self.hobbies
    }
}
}

Создание и использование экземпляров

// Создание и использование объекта в C#
var person = new Person("Alice", 30);
person.AddHobby("Reading");
person.AddHobby("Swimming");

Console.WriteLine(person.GetInfo());
Console.WriteLine($"Hobbies: {string.Join(", ", person.Hobbies)}");

// Прямое изменение свойств
person.Age = 31;
#![allow(unused)]
fn main() {
// Создание и использование структуры в Rust
let mut person = Person::new("Alice".to_string(), 30);
person.add_hobby("Reading".to_string());
person.add_hobby("Swimming".to_string());

println!("{}", person.get_info());
println!("Hobbies: {:?}", person.hobbies());

// Прямое изменение публичных полей
person.age = 31;

// Вывод всей структуры в отладочном формате
println!("{:?}", person);
}

Паттерны инициализации структур

// Инициализация объекта в C#
var person = new Person("Bob", 25)
{
    Hobbies = new List<string> { "Gaming", "Coding" }
};

// Анонимные типы
var anonymous = new { Name = "Charlie", Age = 35 };
#![allow(unused)]
fn main() {
// Инициализация структуры в Rust
let person = Person {
    name: "Bob".to_string(),
    age: 25,
    hobbies: vec!["Gaming".to_string(), "Coding".to_string()],
};

// Синтаксис обновления структуры (аналог spread для объектов)
let older_person = Person {
    age: 26,
    ..person  // Берём остальные поля из person (перемещает person!)
};

// Кортежные структуры (аналог анонимных типов)
#[derive(Debug)]
struct Point(i32, i32);

let point = Point(10, 20);
println!("Point: ({}, {})", point.0, point.1);
}

Методы и ассоциированные функции

Понимание различий между методами и ассоциированными функциями — ключ к успеху.

Виды методов в C#

public class Calculator
{
    private int memory = 0;
    
    // Метод экземпляра
    public int Add(int a, int b)
    {
        return a + b;
    }
    
    // Метод экземпляра, который использует состояние
    public void StoreInMemory(int value)
    {
        memory = value;
    }
    
    // Статический метод
    public static int Multiply(int a, int b)
    {
        return a * b;
    }
    
    // Статический фабричный метод
    public static Calculator CreateWithMemory(int initialMemory)
    {
        var calc = new Calculator();
        calc.memory = initialMemory;
        return calc;
    }
}

Виды методов в Rust

#[derive(Debug)]
pub struct Calculator {
    memory: i32,
}

impl Calculator {
    // Ассоциированная функция (аналог статического метода) — без параметра self
    pub fn new() -> Calculator {
        Calculator { memory: 0 }
    }
    
    // Ассоциированная функция с параметрами
    pub fn with_memory(initial_memory: i32) -> Calculator {
        Calculator { memory: initial_memory }
    }
    
    // Метод, который заимствует неизменяемо (&self)
    pub fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
    
    // Метод, который заимствует изменяемо (&mut self)
    pub fn store_in_memory(&mut self, value: i32) {
        self.memory = value;
    }
    
    // Метод, который забирает владение (self)
    pub fn into_memory(self) -> i32 {
        self.memory  // Calculator потребляется
    }
    
    // Метод-геттер
    pub fn memory(&self) -> i32 {
        self.memory
    }
}

fn main() {
    // Ассоциированные функции вызываются через ::
    let mut calc = Calculator::new();
    let calc2 = Calculator::with_memory(42);
    
    // Методы вызываются через .
    let result = calc.add(5, 3);
    calc.store_in_memory(result);
    
    println!("Memory: {}", calc.memory());
    
    // Метод, потребляющий значение
    let memory_value = calc.into_memory();  // calc больше нельзя использовать
    println!("Final memory: {}", memory_value);
}

Виды получателей методов (receiver)

#![allow(unused)]
fn main() {
impl Person {
    // &self — неизменяемое заимствование (самый частый вариант)
    // Используйте, когда нужно только читать данные
    pub fn get_name(&self) -> &str {
        &self.name
    }
    
    // &mut self — изменяемое заимствование
    // Используйте, когда нужно изменять данные
    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }
    
    // self — забрать владение (встречается реже)
    // Используйте, когда структура должна быть потреблена
    pub fn consume(self) -> String {
        self.name  // Person перемещён, больше недоступен
    }
}

fn method_examples() {
    let mut person = Person::new("Alice".to_string(), 30);
    
    // Неизменяемое заимствование
    let name = person.get_name();  // person по-прежнему можно использовать
    println!("Name: {}", name);
    
    // Изменяемое заимствование
    person.set_name("Alice Smith".to_string());  // person по-прежнему можно использовать
    
    // Забираем владение
    let final_name = person.consume();  // person больше нельзя использовать
    println!("Final name: {}", final_name);
}
}

Упражнения

🏋️ Упражнение: скользящее среднее по срезу (нажмите, чтобы раскрыть)

Задача: напишите функцию, которая принимает срез значений f64 и размер окна, и возвращает Vec<f64> со скользящими средними. Например, [1.0, 2.0, 3.0, 4.0, 5.0] с окном 3 → [2.0, 3.0, 4.0].

fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
    // Ваша реализация здесь
    todo!()
}

fn main() {
    let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let avgs = rolling_average(&data, 3);
    println!("{avgs:?}"); // [2.0, 3.0, 4.0]
}
🔑 Решение
fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
    data.windows(window)
        .map(|w| w.iter().sum::<f64>() / w.len() as f64)
        .collect()
}

fn main() {
    let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let avgs = rolling_average(&data, 3);
    assert_eq!(avgs, vec![2.0, 3.0, 4.0]);
    println!("{avgs:?}");
}

Ключевая мысль: у срезов есть мощные встроенные методы вроде .windows(), .chunks() и .split(), которые заменяют ручную арифметику индексов. В C# вы бы использовали Enumerable.Range или LINQ .Skip().Take().

🏋️ Упражнение: мини-адресная книга (нажмите, чтобы раскрыть)

Создайте небольшую адресную книгу с использованием структур, перечислений и методов:

  1. Определите перечисление PhoneType { Mobile, Home, Work }
  2. Определите структуру Contact с полями name: String и phones: Vec<(PhoneType, String)>
  3. Реализуйте Contact::new(name: impl Into<String>) -> Self
  4. Реализуйте Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)
  5. Реализуйте Contact::mobile_numbers(&self) -> Vec<&str>, который возвращает только мобильные номера
  6. В main создайте контакт, добавьте два телефона и выведите мобильные номера
🔑 Решение
#[derive(Debug, PartialEq)]
enum PhoneType { Mobile, Home, Work }

#[derive(Debug)]
struct Contact {
    name: String,
    phones: Vec<(PhoneType, String)>,
}

impl Contact {
    fn new(name: impl Into<String>) -> Self {
        Contact { name: name.into(), phones: Vec::new() }
    }

    fn add_phone(&mut self, kind: PhoneType, number: impl Into<String>) {
        self.phones.push((kind, number.into()));
    }

    fn mobile_numbers(&self) -> Vec<&str> {
        self.phones
            .iter()
            .filter(|(kind, _)| *kind == PhoneType::Mobile)
            .map(|(_, num)| num.as_str())
            .collect()
    }
}

fn main() {
    let mut alice = Contact::new("Alice");
    alice.add_phone(PhoneType::Mobile, "+1-555-0100");
    alice.add_phone(PhoneType::Work, "+1-555-0200");
    alice.add_phone(PhoneType::Mobile, "+1-555-0101");

    println!("Мобильные номера {}: {:?}", alice.name, alice.mobile_numbers());
}

Паттерны конструкторов

Паттерны конструкторов

Что вы узнаете: как создавать структуры Rust без традиционных конструкторов — соглашения new(), трейт Default, фабричные методы и паттерн builder для сложной инициализации.

Сложность: 🟢 Начальный

Паттерны конструкторов в C#

public class Configuration
{
    public string DatabaseUrl { get; set; }
    public int MaxConnections { get; set; }
    public bool EnableLogging { get; set; }
    
    // Конструктор по умолчанию
    public Configuration()
    {
        DatabaseUrl = "localhost";
        MaxConnections = 10;
        EnableLogging = false;
    }
    
    // Конструктор с параметрами
    public Configuration(string databaseUrl, int maxConnections)
    {
        DatabaseUrl = databaseUrl;
        MaxConnections = maxConnections;
        EnableLogging = false;
    }
    
    // Фабричный метод
    public static Configuration ForProduction()
    {
        return new Configuration("prod.db.server", 100)
        {
            EnableLogging = true
        };
    }
}

Паттерны конструкторов в Rust

#[derive(Debug)]
pub struct Configuration {
    pub database_url: String,
    pub max_connections: u32,
    pub enable_logging: bool,
}

impl Configuration {
    // Конструктор по умолчанию
    pub fn new() -> Configuration {
        Configuration {
            database_url: "localhost".to_string(),
            max_connections: 10,
            enable_logging: false,
        }
    }
    
    // Конструктор с параметрами
    pub fn with_database(database_url: String, max_connections: u32) -> Configuration {
        Configuration {
            database_url,
            max_connections,
            enable_logging: false,
        }
    }
    
    // Фабричный метод
    pub fn for_production() -> Configuration {
        Configuration {
            database_url: "prod.db.server".to_string(),
            max_connections: 100,
            enable_logging: true,
        }
    }
    
    // Метод в стиле builder
    pub fn enable_logging(mut self) -> Configuration {
        self.enable_logging = true;
        self  // Возвращаем self для цепочки вызовов
    }
    
    pub fn max_connections(mut self, count: u32) -> Configuration {
        self.max_connections = count;
        self
    }
}

// Реализация трейта Default
impl Default for Configuration {
    fn default() -> Self {
        Self::new()
    }
}

fn main() {
    // Разные способы создания
    let config1 = Configuration::new();
    let config2 = Configuration::with_database("localhost:5432".to_string(), 20);
    let config3 = Configuration::for_production();
    
    // Паттерн builder
    let config4 = Configuration::new()
        .enable_logging()
        .max_connections(50);
    
    // Использование трейта Default
    let config5 = Configuration::default();
    
    println!("{:?}", config4);
}

Реализация паттерна Builder

// Более сложный паттерн builder
#[derive(Debug)]
pub struct DatabaseConfig {
    host: String,
    port: u16,
    username: String,
    password: Option<String>,
    ssl_enabled: bool,
    timeout_seconds: u64,
}

pub struct DatabaseConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
    username: Option<String>,
    password: Option<String>,
    ssl_enabled: bool,
    timeout_seconds: u64,
}

impl DatabaseConfigBuilder {
    pub fn new() -> Self {
        DatabaseConfigBuilder {
            host: None,
            port: None,
            username: None,
            password: None,
            ssl_enabled: false,
            timeout_seconds: 30,
        }
    }
    
    pub fn host(mut self, host: impl Into<String>) -> Self {
        self.host = Some(host.into());
        self
    }
    
    pub fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }
    
    pub fn username(mut self, username: impl Into<String>) -> Self {
        self.username = Some(username.into());
        self
    }
    
    pub fn password(mut self, password: impl Into<String>) -> Self {
        self.password = Some(password.into());
        self
    }
    
    pub fn enable_ssl(mut self) -> Self {
        self.ssl_enabled = true;
        self
    }
    
    pub fn timeout(mut self, seconds: u64) -> Self {
        self.timeout_seconds = seconds;
        self
    }
    
    pub fn build(self) -> Result<DatabaseConfig, String> {
        let host = self.host.ok_or("Host is required")?;
        let port = self.port.ok_or("Port is required")?;
        let username = self.username.ok_or("Username is required")?;
        
        Ok(DatabaseConfig {
            host,
            port,
            username,
            password: self.password,
            ssl_enabled: self.ssl_enabled,
            timeout_seconds: self.timeout_seconds,
        })
    }
}

fn main() {
    let config = DatabaseConfigBuilder::new()
        .host("localhost")
        .port(5432)
        .username("admin")
        .password("secret123")
        .enable_ssl()
        .timeout(60)
        .build()
        .expect("Failed to build config");
    
    println!("{:?}", config);
}

Упражнения

🏋️ Упражнение: builder с валидацией (нажмите, чтобы раскрыть)

Создайте EmailBuilder, который:

  1. Требует to и subject (builder не должен компилироваться без них — используйте typestate или проверяйте в build())
  2. Имеет необязательные body и cc (Vec адресов)
  3. build() возвращает Result<Email, String> — отвергает пустые to или subject
  4. Напишите тесты, доказывающие, что некорректные входные данные отвергаются
🔑 Решение
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Email {
    to: String,
    subject: String,
    body: Option<String>,
    cc: Vec<String>,
}

#[derive(Default)]
struct EmailBuilder {
    to: Option<String>,
    subject: Option<String>,
    body: Option<String>,
    cc: Vec<String>,
}

impl EmailBuilder {
    fn new() -> Self { Self::default() }

    fn to(mut self, to: impl Into<String>) -> Self {
        self.to = Some(to.into()); self
    }
    fn subject(mut self, subject: impl Into<String>) -> Self {
        self.subject = Some(subject.into()); self
    }
    fn body(mut self, body: impl Into<String>) -> Self {
        self.body = Some(body.into()); self
    }
    fn cc(mut self, addr: impl Into<String>) -> Self {
        self.cc.push(addr.into()); self
    }
    fn build(self) -> Result<Email, String> {
        let to = self.to.filter(|s| !s.is_empty())
            .ok_or("'to' is required")?;
        let subject = self.subject.filter(|s| !s.is_empty())
            .ok_or("'subject' is required")?;
        Ok(Email { to, subject, body: self.body, cc: self.cc })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn valid_email() {
        let email = EmailBuilder::new()
            .to("alice@example.com")
            .subject("Hello")
            .build();
        assert!(email.is_ok());
    }
    #[test]
    fn missing_to_fails() {
        let email = EmailBuilder::new().subject("Hello").build();
        assert!(email.is_err());
    }
}
}

Коллекции — Vec, HashMap и итераторы

Vec<T> против List<T>

Что вы узнаете: Vec<T> против List<T>, HashMap против Dictionary, безопасные паттерны доступа (почему Rust возвращает Option вместо генерации исключения), а также последствия владения для коллекций.

Сложность: 🟢 Начальный

Vec<T> — аналог List<T> из C# в Rust, но с семантикой владения.

List<T> в C#

// List<T> в C# — ссылочный тип, размещается в куче
var numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);

// Передача в метод — ссылка копируется
ProcessList(numbers);
Console.WriteLine(numbers.Count);  // По-прежнему доступен

void ProcessList(List<int> list)
{
    list.Add(4);  // Изменяет исходный список
    Console.WriteLine($"Count in method: {list.Count}");
}

Vec<T> в Rust

#![allow(unused)]
fn main() {
// Vec<T> в Rust — владеемый тип, размещается в куче
let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);

// Метод, который забирает владение
process_vec(numbers);
// println!("{:?}", numbers);  // ❌ Ошибка: numbers было перемещено

// Метод, который заимствует
let mut numbers = vec![1, 2, 3];  // Макрос vec! для удобства
process_vec_borrowed(&mut numbers);
println!("{:?}", numbers);  // ✅ По-прежнему доступен

fn process_vec(mut vec: Vec<i32>) {  // Забирает владение
    vec.push(4);
    println!("Count in method: {}", vec.len());
    // vec освобождается здесь
}

fn process_vec_borrowed(vec: &mut Vec<i32>) {  // Заимствует изменяемо
    vec.push(4);
    println!("Count in method: {}", vec.len());
}
}

Создание и инициализация векторов

// Инициализация List в C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var empty = new List<int>();
var sized = new List<int>(10);  // Начальная ёмкость

// Из других коллекций
var fromArray = new List<int>(new[] { 1, 2, 3 });
#![allow(unused)]
fn main() {
// Инициализация Vec в Rust
let numbers = vec![1, 2, 3, 4, 5];  // Макрос vec!
let empty: Vec<i32> = Vec::new();   // Для пустого нужна аннотация типа
let sized = Vec::with_capacity(10); // Предварительное выделение ёмкости

// Из итератора
let from_range: Vec<i32> = (1..=5).collect();
let from_array = vec![1, 2, 3];
}

Сравнение распространённых операций

// Операции с List в C#
var list = new List<int> { 1, 2, 3 };

list.Add(4);                    // Добавить элемент
list.Insert(0, 0);              // Вставить по индексу
list.Remove(2);                 // Удалить первое вхождение
list.RemoveAt(1);               // Удалить по индексу
list.Clear();                   // Удалить все

int first = list[0];            // Доступ по индексу
int count = list.Count;         // Получить количество
bool contains = list.Contains(3); // Проверка наличия
#![allow(unused)]
fn main() {
// Операции с Vec в Rust
let mut vec = vec![1, 2, 3];

vec.push(4);                    // Добавить элемент
vec.insert(0, 0);               // Вставить по индексу
vec.retain(|&x| x != 2);        // Удалить элементы (в функциональном стиле)
vec.remove(1);                  // Удалить по индексу
vec.clear();                    // Удалить все

let first = vec[0];             // Доступ по индексу (паника, если выход за границы)
let safe_first = vec.get(0);    // Безопасный доступ, возвращает Option<&T>
let count = vec.len();          // Получить количество
let contains = vec.contains(&3); // Проверка наличия
}

Паттерны безопасного доступа

// C# — проверка границ через исключения
public int SafeAccess(List<int> list, int index)
{
    try
    {
        return list[index];
    }
    catch (ArgumentOutOfRangeException)
    {
        return -1;  // Значение по умолчанию
    }
}
// Rust — безопасный доступ через Option
fn safe_access(vec: &[i32], index: usize) -> Option<i32> {
    vec.get(index).copied()  // Возвращает Option<i32>
}

fn main() {
    let vec = vec![1, 2, 3];
    
    // Паттерны безопасного доступа
    match vec.get(10) {
        Some(value) => println!("Value: {}", value),
        None => println!("Индекс вне границ"),
    }
    
    // Или через unwrap_or
    let value = vec.get(10).copied().unwrap_or(-1);
    println!("Value: {}", value);
}

HashMap против Dictionary

HashMap — аналог Dictionary<K,V> из C# в Rust.

Dictionary в C#

// Dictionary<TKey, TValue> в C#
var scores = new Dictionary<string, int>
{
    ["Alice"] = 100,
    ["Bob"] = 85,
    ["Charlie"] = 92
};

// Добавление/обновление
scores["Dave"] = 78;
scores["Alice"] = 105;  // Обновление существующего

// Безопасный доступ
if (scores.TryGetValue("Eve", out int score))
{
    Console.WriteLine($"Eve's score: {score}");
}
else
{
    Console.WriteLine("Eve not found");
}

// Итерация
foreach (var kvp in scores)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

HashMap в Rust

#![allow(unused)]
fn main() {
use std::collections::HashMap;

// Создание и инициализация HashMap
let mut scores = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);
scores.insert("Charlie".to_string(), 92);

// Или из итератора
let scores: HashMap<String, i32> = [
    ("Alice".to_string(), 100),
    ("Bob".to_string(), 85),
    ("Charlie".to_string(), 92),
].into_iter().collect();

// Добавление/обновление
let mut scores = scores;  // Делаем изменяемой
scores.insert("Dave".to_string(), 78);
scores.insert("Alice".to_string(), 105);  // Обновление существующего

// Безопасный доступ
match scores.get("Eve") {
    Some(score) => println!("Eve's score: {}", score),
    None => println!("Eve not found"),
}

// Итерация
for (name, score) in &scores {
    println!("{}: {}", name, score);
}
}

Операции с HashMap

// Операции с Dictionary в C#
var dict = new Dictionary<string, int>();

dict["key"] = 42;                    // Вставка/обновление
bool exists = dict.ContainsKey("key"); // Проверка наличия
bool removed = dict.Remove("key");    // Удаление
dict.Clear();                        // Очистка

// Получение со значением по умолчанию
int value = dict.GetValueOrDefault("missing", 0);
#![allow(unused)]
fn main() {
use std::collections::HashMap;

// Операции с HashMap в Rust
let mut map = HashMap::new();

map.insert("key".to_string(), 42);   // Вставка/обновление
let exists = map.contains_key("key"); // Проверка наличия
let removed = map.remove("key");      // Удаление, возвращает Option<V>
map.clear();                         // Очистка

// Entry API для продвинутых операций
let mut map = HashMap::new();
map.entry("key".to_string()).or_insert(42);  // Вставить, если не существует
map.entry("key".to_string()).and_modify(|v| *v += 1); // Изменить, если существует

// Получение со значением по умолчанию
let value = map.get("missing").copied().unwrap_or(0);
}

Владение ключами и значениями HashMap

#![allow(unused)]
fn main() {
// Понимание владения в HashMap
fn ownership_example() {
    let mut map = HashMap::new();
    
    // Строковые ключи и значения перемещаются в map
    let key = String::from("name");
    let value = String::from("Alice");
    
    map.insert(key, value);
    // println!("{}", key);   // ❌ Ошибка: key было перемещено
    // println!("{}", value); // ❌ Ошибка: value было перемещено
    
    // Доступ через ссылки
    if let Some(name) = map.get("name") {
        println!("Name: {}", name);  // Заимствуем значение
    }
}

// Использование ключей &str (без передачи владения)
fn string_slice_keys() {
    let mut map = HashMap::new();
    
    map.insert("name", "Alice");     // Ключи и значения типа &str
    map.insert("age", "30");
    
    // Со строковыми литералами проблем с владением нет
    println!("Name exists: {}", map.contains_key("name"));
}
}

Работа с коллекциями

Паттерны итерации

// Паттерны итерации в C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Цикл for с индексом
for (int i = 0; i < numbers.Count; i++)
{
    Console.WriteLine($"Index {i}: {numbers[i]}");
}

// Цикл foreach
foreach (int num in numbers)
{
    Console.WriteLine(num);
}

// Методы LINQ
var doubled = numbers.Select(x => x * 2).ToList();
var evens = numbers.Where(x => x % 2 == 0).ToList();
#![allow(unused)]
fn main() {
// Паттерны итерации в Rust
let numbers = vec![1, 2, 3, 4, 5];

// Цикл for с индексом
for (i, num) in numbers.iter().enumerate() {
    println!("Index {}: {}", i, num);
}

// Цикл for по значениям
for num in &numbers {  // Заимствуем каждый элемент
    println!("{}", num);
}

// Методы итераторов (как LINQ)
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
let evens: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect();

// Или эффективнее — потребляющий итератор
let doubled: Vec<i32> = numbers.into_iter().map(|x| x * 2).collect();
}

Iterator против IntoIterator против Iter

#![allow(unused)]
fn main() {
// Понимание различных способов итерации
fn iteration_methods() {
    let vec = vec![1, 2, 3, 4, 5];
    
    // 1. iter() — заимствует элементы (&T)
    for item in vec.iter() {
        println!("{}", item);  // item имеет тип &i32
    }
    // vec здесь по-прежнему доступен
    
    // 2. into_iter() — забирает владение (T)
    for item in vec.into_iter() {
        println!("{}", item);  // item имеет тип i32
    }
    // vec здесь больше недоступен
    
    let mut vec = vec![1, 2, 3, 4, 5];
    
    // 3. iter_mut() — изменяемые заимствования (&mut T)
    for item in vec.iter_mut() {
        *item *= 2;  // item имеет тип &mut i32
    }
    println!("{:?}", vec);  // [2, 4, 6, 8, 10]
}
}

Сбор результатов

// C# — обработка коллекций с возможными ошибками
public List<int> ParseNumbers(List<string> inputs)
{
    var results = new List<int>();
    foreach (string input in inputs)
    {
        if (int.TryParse(input, out int result))
        {
            results.Add(result);
        }
        // Молча пропускаем некорректные значения
    }
    return results;
}
// Rust — явная обработка ошибок через collect
fn parse_numbers(inputs: Vec<String>) -> Result<Vec<i32>, std::num::ParseIntError> {
    inputs.into_iter()
        .map(|s| s.parse::<i32>())  // Возвращает Result<i32, ParseIntError>
        .collect()                  // Собирает в Result<Vec<i32>, ParseIntError>
}

// Альтернатива: отфильтровать ошибки
fn parse_numbers_filter(inputs: Vec<String>) -> Vec<i32> {
    inputs.into_iter()
        .filter_map(|s| s.parse::<i32>().ok())  // Оставляем только значения Ok
        .collect()
}

fn main() {
    let inputs = vec!["1".to_string(), "2".to_string(), "invalid".to_string(), "4".to_string()];
    
    // Вариант, который завершается при первой ошибке
    match parse_numbers(inputs.clone()) {
        Ok(numbers) => println!("All parsed: {:?}", numbers),
        Err(error) => println!("Parse error: {}", error),
    }
    
    // Вариант, который пропускает ошибки
    let numbers = parse_numbers_filter(inputs);
    println!("Successfully parsed: {:?}", numbers);  // [1, 2, 4]
}

Упражнения

🏋️ Упражнение: из LINQ в итераторы (нажмите, чтобы раскрыть)

Переведите этот LINQ-запрос на C# в идиоматичные итераторы Rust:

var result = students
    .Where(s => s.Grade >= 90)
    .OrderByDescending(s => s.Grade)
    .Select(s => $"{s.Name}: {s.Grade}")
    .Take(3)
    .ToList();

Используйте эту структуру:

#![allow(unused)]
fn main() {
struct Student { name: String, grade: u32 }
}

Верните Vec<String> из трёх лучших студентов с оценкой ≥ 90, отформатированных как "Name: Grade".

🔑 Решение
#[derive(Debug)]
struct Student { name: String, grade: u32 }

fn top_students(students: &mut [Student]) -> Vec<String> {
    students.sort_by(|a, b| b.grade.cmp(&a.grade)); // сортировка по убыванию
    students.iter()
        .filter(|s| s.grade >= 90)
        .take(3)
        .map(|s| format!("{}: {}", s.name, s.grade))
        .collect()
}

fn main() {
    let mut students = vec![
        Student { name: "Alice".into(), grade: 95 },
        Student { name: "Bob".into(), grade: 88 },
        Student { name: "Carol".into(), grade: 92 },
        Student { name: "Dave".into(), grade: 97 },
        Student { name: "Eve".into(), grade: 91 },
    ];
    let result = top_students(&mut students);
    assert_eq!(result, vec!["Dave: 97", "Alice: 95", "Carol: 92"]);
    println!("{result:?}");
}

Ключевое отличие от C#: итераторы Rust ленивы (как LINQ), но .sort_by() — энергичная операция, которая сортирует на месте; ленивого OrderBy здесь нет. Сначала сортируете, затем строите цепочку ленивых операций.


6. Перечисления и сопоставление с образцом

Алгебраические типы данных против объединений C#

Что вы узнаете: алгебраические типы данных Rust (перечисления с данными) в сравнении с ограниченными дискриминируемыми объединениями C#, выражения match с проверкой исчерпываемости, защитные условия (guards) и деструктуризацию вложенных паттернов.

Сложность: 🟡 Средний

Дискриминируемые объединения в C# (ограниченные)

// C# — ограниченная поддержка объединений через наследование
public abstract class Result
{
    public abstract T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError);
}

public class Success : Result
{
    public string Value { get; }
    public Success(string value) => Value = value;
    
    public override T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError)
        => onSuccess(this);
}

public class Error : Result
{
    public string Message { get; }
    public Error(string message) => Message = message;
    
    public override T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError)
        => onError(this);
}

// Records с сопоставлением с образцом в C# 9+ (лучше)
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;

public static double Area(Shape shape) => shape switch
{
    Circle(var radius) => Math.PI * radius * radius,
    Rectangle(var width, var height) => width * height,
    _ => throw new ArgumentException("Unknown shape")  // [ERROR] Возможна ошибка времени выполнения
};

Алгебраические типы данных Rust (перечисления)

#![allow(unused)]
fn main() {
// Rust — настоящие алгебраические типы данных с исчерпывающим сопоставлением с образцом
#[derive(Debug, Clone)]
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

#[derive(Debug, Clone)]
pub enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

impl Shape {
    pub fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { base, height } => 0.5 * base * height,
            // [OK] Ошибка компиляции, если какой-либо вариант пропущен!
        }
    }
}

// Продвинутый пример: перечисления могут хранить значения разных типов
#[derive(Debug)]
pub enum Value {
    Integer(i64),
    Float(f64),
    Text(String),
    Boolean(bool),
    List(Vec<Value>),  // Рекурсивные типы!
}

impl Value {
    pub fn type_name(&self) -> &'static str {
        match self {
            Value::Integer(_) => "integer",
            Value::Float(_) => "float",
            Value::Text(_) => "text",
            Value::Boolean(_) => "boolean",
            Value::List(_) => "list",
        }
    }
}
}
graph TD
    subgraph "Дискриминируемые объединения C# (обходные пути)"
        CS_ABSTRACT["abstract class Result"]
        CS_SUCCESS["class Success : Result"]
        CS_ERROR["class Error : Result"]
        CS_MATCH["Ручной метод Match<br/>или switch-выражения"]
        CS_RUNTIME["[ERROR] Исключения во время выполнения<br/>для пропущенных случаев"]
        CS_HEAP["[ERROR] Выделение в куче<br/>для наследования классов"]
        
        CS_ABSTRACT --> CS_SUCCESS
        CS_ABSTRACT --> CS_ERROR
        CS_SUCCESS --> CS_MATCH
        CS_ERROR --> CS_MATCH
        CS_MATCH --> CS_RUNTIME
        CS_ABSTRACT --> CS_HEAP
    end
    
    subgraph "Алгебраические типы данных Rust"
        RUST_ENUM["enum Shape { ... }"]
        RUST_VARIANTS["Circle { radius }<br/>Rectangle { width, height }<br/>Triangle { base, height }"]
        RUST_MATCH["match shape { ... }"]
        RUST_EXHAUSTIVE["[OK] Проверка исчерпываемости<br/>Гарантия на этапе компиляции"]
        RUST_STACK["[OK] Размещение на стеке<br/>Эффективное использование памяти"]
        RUST_ZERO["[OK] Абстракция с нулевой стоимостью"]
        
        RUST_ENUM --> RUST_VARIANTS
        RUST_VARIANTS --> RUST_MATCH
        RUST_MATCH --> RUST_EXHAUSTIVE
        RUST_ENUM --> RUST_STACK
        RUST_STACK --> RUST_ZERO
    end
    
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style CS_HEAP fill:#fff3e0,color:#000
    style RUST_EXHAUSTIVE fill:#c8e6c9,color:#000
    style RUST_STACK fill:#c8e6c9,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000

Перечисления и сопоставление с образцом

Перечисления Rust намного мощнее перечислений C# — они могут хранить данные и составляют основу типобезопасного программирования.

Ограничения перечислений C#

// Перечисление C# — всего лишь именованные константы
public enum Status
{
    Pending,
    Approved,
    Rejected
}

// Перечисление C# с базовыми значениями
public enum HttpStatusCode
{
    OK = 200,
    NotFound = 404,
    InternalServerError = 500
}

// Для сложных данных нужны отдельные классы
public abstract class Result
{
    public abstract bool IsSuccess { get; }
}

public class Success : Result
{
    public string Value { get; }
    public override bool IsSuccess => true;
    
    public Success(string value)
    {
        Value = value;
    }
}

public class Error : Result
{
    public string Message { get; }
    public override bool IsSuccess => false;
    
    public Error(string message)
    {
        Message = message;
    }
}

Мощь перечислений Rust

#![allow(unused)]
fn main() {
// Простое перечисление (как enum в C#)
#[derive(Debug, PartialEq)]
enum Status {
    Pending,
    Approved,
    Rejected,
}

// Перечисление с данными (здесь Rust раскрывается!)
#[derive(Debug)]
enum Result<T, E> {
    Ok(T),      // Вариант успеха, хранящий значение типа T
    Err(E),     // Вариант ошибки, хранящий ошибку типа E
}

// Сложное перечисление с данными разных типов
#[derive(Debug)]
enum Message {
    Quit,                       // Без данных
    Move { x: i32, y: i32 },   // Вариант в виде структуры
    Write(String),             // Вариант в виде кортежа
    ChangeColor(i32, i32, i32), // Несколько значений
}

// Пример из практики: HTTP-ответ
#[derive(Debug)]
enum HttpResponse {
    Ok { body: String, headers: Vec<String> },
    NotFound { path: String },
    InternalError { message: String, code: u16 },
    Redirect { location: String },
}
}

Сопоставление с образцом через match

// Оператор switch в C# (ограниченный)
public string HandleStatus(Status status)
{
    switch (status)
    {
        case Status.Pending:
            return "Waiting for approval";
        case Status.Approved:
            return "Request approved";
        case Status.Rejected:
            return "Request rejected";
        default:
            return "Unknown status"; // Всегда нужен default
    }
}

// Сопоставление с образцом в C# (C# 8+)
public string HandleResult(Result result)
{
    return result switch
    {
        Success success => $"Success: {success.Value}",
        Error error => $"Error: {error.Message}",
        _ => "Unknown result" // Всё равно нужен перехватывающий случай
    };
}
#![allow(unused)]
fn main() {
// match в Rust — исчерпывающий и мощный
fn handle_status(status: Status) -> String {
    match status {
        Status::Pending => "Waiting for approval".to_string(),
        Status::Approved => "Request approved".to_string(),
        Status::Rejected => "Request rejected".to_string(),
        // Default не нужен — компилятор гарантирует исчерпываемость
    }
}

// Сопоставление с образцом с извлечением данных
fn handle_result<T, E>(result: Result<T, E>) -> String 
where 
    T: std::fmt::Debug,
    E: std::fmt::Debug,
{
    match result {
        Result::Ok(value) => format!("Success: {:?}", value),
        Result::Err(error) => format!("Error: {:?}", error),
        // Исчерпывающе — default не нужен
    }
}

// Сложное сопоставление с образцом
fn handle_message(msg: Message) -> String {
    match msg {
        Message::Quit => "Goodbye!".to_string(),
        Message::Move { x, y } => format!("Move to ({}, {})", x, y),
        Message::Write(text) => format!("Write: {}", text),
        Message::ChangeColor(r, g, b) => format!("Change color to RGB({}, {}, {})", r, g, b),
    }
}

// Обработка HTTP-ответа
fn handle_http_response(response: HttpResponse) -> String {
    match response {
        HttpResponse::Ok { body, headers } => {
            format!("Success! Body: {}, Headers: {:?}", body, headers)
        },
        HttpResponse::NotFound { path } => {
            format!("404: Path '{}' not found", path)
        },
        HttpResponse::InternalError { message, code } => {
            format!("Error {}: {}", code, message)
        },
        HttpResponse::Redirect { location } => {
            format!("Redirect to: {}", location)
        },
    }
}
}

Защитные условия и продвинутые паттерны

#![allow(unused)]
fn main() {
// Сопоставление с образцом с защитными условиями (guards)
fn describe_number(x: i32) -> String {
    match x {
        n if n < 0 => "negative".to_string(),
        0 => "zero".to_string(),
        n if n < 10 => "single digit".to_string(),
        n if n < 100 => "double digit".to_string(),
        _ => "large number".to_string(),
    }
}

// Сопоставление с диапазонами
fn describe_age(age: u32) -> String {
    match age {
        0..=12 => "child".to_string(),
        13..=19 => "teenager".to_string(),
        20..=64 => "adult".to_string(),
        65.. => "senior".to_string(),
    }
}

// Деструктуризация структур и кортежей
}
🏋️ Упражнение: парсер команд (нажмите, чтобы раскрыть)

Задача: смоделируйте систему CLI-команд с помощью перечислений Rust. Разберите строку ввода в перечисление Command и выполните каждый вариант. Обработайте неизвестные команды с правильной обработкой ошибок.

#![allow(unused)]
fn main() {
// Стартовый код — заполните пропуски
#[derive(Debug)]
enum Command {
    // TODO: Добавьте варианты Quit, Echo(String), Move { x: i32, y: i32 }, Count(u32)
}

fn parse_command(input: &str) -> Result<Command, String> {
    let parts: Vec<&str> = input.splitn(2, ' ').collect();
    // TODO: сопоставьте parts[0] и разберите аргументы
    todo!()
}

fn execute(cmd: &Command) -> String {
    // TODO: сопоставьте каждый вариант и верните описание
    todo!()
}
}
🔑 Решение
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum Command {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
    Count(u32),
}

fn parse_command(input: &str) -> Result<Command, String> {
    let parts: Vec<&str> = input.splitn(2, ' ').collect();
    match parts[0] {
        "quit" => Ok(Command::Quit),
        "echo" => {
            let msg = parts.get(1).unwrap_or(&"").to_string();
            Ok(Command::Echo(msg))
        }
        "move" => {
            let args = parts.get(1).ok_or("move requires 'x y'")?;
            let coords: Vec<&str> = args.split_whitespace().collect();
            let x = coords.get(0).ok_or("missing x")?.parse::<i32>().map_err(|e| e.to_string())?;
            let y = coords.get(1).ok_or("missing y")?.parse::<i32>().map_err(|e| e.to_string())?;
            Ok(Command::Move { x, y })
        }
        "count" => {
            let n = parts.get(1).ok_or("count requires a number")?
                .parse::<u32>().map_err(|e| e.to_string())?;
            Ok(Command::Count(n))
        }
        other => Err(format!("Unknown command: {other}")),
    }
}

fn execute(cmd: &Command) -> String {
    match cmd {
        Command::Quit           => "Goodbye!".to_string(),
        Command::Echo(msg)      => msg.clone(),
        Command::Move { x, y }  => format!("Moving to ({x}, {y})"),
        Command::Count(n)       => format!("Counted to {n}"),
    }
}
}

Ключевые выводы:

  • Каждый вариант перечисления может хранить разные данные — иерархии классов не нужны
  • match заставляет обработать каждый вариант, что предотвращает забытые случаи
  • Оператор ? аккуратно связывает распространение ошибок — никаких вложенных try-catch

Исчерпывающее сопоставление и null-безопасность

Исчерпывающее сопоставление с образцом: гарантии компилятора против ошибок времени выполнения

Что вы узнаете: почему выражения switch в C# молча пропускают случаи, тогда как match в Rust ловит их на этапе компиляции, Option<T> против Nullable<T> для null-безопасности, а также собственные типы ошибок с Result<T, E>.

Сложность: 🟡 Средний

Выражения switch в C# — всё ещё неполные

// Выражения switch в C# выглядят исчерпывающими, но это не гарантировано
public enum HttpStatus { Ok, NotFound, ServerError, Unauthorized }

public string HandleResponse(HttpStatus status) => status switch
{
    HttpStatus.Ok => "Success",
    HttpStatus.NotFound => "Resource not found",
    HttpStatus.ServerError => "Internal error",
    // Нет варианта Unauthorized — компилируется с предупреждением CS8524, но это НЕ ошибка!
    // Во время выполнения: SwitchExpressionException, если status равен Unauthorized
};

// Даже с предупреждениями о nullable этот код компилируется:
public class User 
{
    public string Name { get; set; }
    public bool IsActive { get; set; }
}

public string ProcessUser(User? user) => user switch
{
    { IsActive: true } => $"Active: {user.Name}",
    { IsActive: false } => $"Inactive: {user.Name}",
    // Нет случая для null — предупреждение компилятора CS8655, но НЕ ошибка!
    // Во время выполнения: SwitchExpressionException, если user равен null
};
// Добавление варианта в перечисление позже не ломает компиляцию существующих switch
public enum HttpStatus 
{ 
    Ok, 
    NotFound, 
    ServerError, 
    Unauthorized,
    Forbidden  // Добавление этого даёт ещё одно предупреждение CS8524, но не ломает компиляцию!
}

Сопоставление с образцом в Rust — настоящая исчерпываемость

#![allow(unused)]
fn main() {
#[derive(Debug)]
enum HttpStatus {
    Ok,
    NotFound, 
    ServerError,
    Unauthorized,
}

fn handle_response(status: HttpStatus) -> &'static str {
    match status {
        HttpStatus::Ok => "Success",
        HttpStatus::NotFound => "Resource not found", 
        HttpStatus::ServerError => "Internal error",
        HttpStatus::Unauthorized => "Authentication required",
        // Ошибка компилятора, если пропущен любой случай!
        // Этот код буквально не скомпилируется
    }
}

// Добавление нового варианта ломает компиляцию везде, где он используется
#[derive(Debug)]
enum HttpStatus {
    Ok,
    NotFound,
    ServerError, 
    Unauthorized,
    Forbidden,  // Добавление этого ломает компиляцию в handle_response()
}
// Компилятор заставляет обработать ВСЕ случаи

// Сопоставление с образцом для Option<T> тоже исчерпывающее
fn process_optional_value(value: Option<i32>) -> String {
    match value {
        Some(n) => format!("Got value: {}", n),
        None => "No value".to_string(),
        // Пропуск любого из случаев = ошибка компиляции
    }
}
}
graph TD
    subgraph "Ограничения сопоставления с образцом в C#"
        CS_SWITCH["switch-выражение"]
        CS_WARNING["⚠️ Только предупреждения компилятора"]
        CS_COMPILE["✅ Компилируется успешно"]
        CS_RUNTIME["💥 Исключения во время выполнения"]
        CS_DEPLOY["❌ Ошибки попадают в продакшен"]
        CS_SILENT["😰 Тихие сбои при изменении перечислений"]
        
        CS_SWITCH --> CS_WARNING
        CS_WARNING --> CS_COMPILE
        CS_COMPILE --> CS_RUNTIME
        CS_RUNTIME --> CS_DEPLOY
        CS_SWITCH --> CS_SILENT
    end
    
    subgraph "Исчерпывающее сопоставление в Rust"
        RUST_MATCH["match-выражение"]
        RUST_ERROR["🛑 Компиляция падает"]
        RUST_FIX["✅ Нужно обработать все случаи"]
        RUST_SAFE["✅ Никаких сюрпризов во время выполнения"]
        RUST_EVOLUTION["🔄 Изменения перечислений ломают компиляцию"]
        RUST_REFACTOR["🛠️ Принудительный рефакторинг"]
        
        RUST_MATCH --> RUST_ERROR
        RUST_ERROR --> RUST_FIX
        RUST_FIX --> RUST_SAFE
        RUST_MATCH --> RUST_EVOLUTION
        RUST_EVOLUTION --> RUST_REFACTOR
    end
    
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style CS_DEPLOY fill:#ffcdd2,color:#000
    style CS_SILENT fill:#ffcdd2,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000
    style RUST_REFACTOR fill:#c8e6c9,color:#000

Null-безопасность: Nullable<T> против Option<T>

Эволюция работы с null в C#

// C# — традиционная работа с null (подвержена ошибкам)
public class User
{
    public string Name { get; set; }  // Может быть null!
    public string Email { get; set; } // Может быть null!
}

public string GetUserDisplayName(User user)
{
    if (user?.Name != null)  // Оператор null-условного доступа
    {
        return user.Name;
    }
    return "Unknown User";
}
// Nullable-ссылочные типы C# 8+
public class User
{
    public string Name { get; set; }    // Не допускает null
    public string? Email { get; set; }  // Явно допускает null
}

// Nullable<T> в C# для значимых типов
int? maybeNumber = GetNumber();
if (maybeNumber.HasValue)
{
    Console.WriteLine(maybeNumber.Value);
}

Система Option<T> в Rust

#![allow(unused)]
fn main() {
// Rust — явная работа с null через Option<T>
#[derive(Debug)]
pub struct User {
    name: String,           // Никогда не null
    email: Option<String>,  // Явно необязательное
}

impl User {
    pub fn get_display_name(&self) -> &str {
        &self.name  // Проверка на null не нужна — значение гарантированно существует
    }
    
    pub fn get_email_or_default(&self) -> String {
        self.email
            .as_ref()
            .map(|e| e.clone())
            .unwrap_or_else(|| "no-email@example.com".to_string())
    }
}

// Сопоставление с образцом заставляет обработать случай None
fn handle_optional_user(user: Option<User>) {
    match user {
        Some(u) => println!("User: {}", u.get_display_name()),
        None => println!("No user found"),
        // Ошибка компилятора, если случай None не обработан!
    }
}
}
graph TD
    subgraph "Эволюция работы с null в C#"
        CS_NULL["Традиционно: string name<br/>[ERROR] Может быть null"]
        CS_NULLABLE["Nullable<T>: int? value<br/>[OK] Явно для значимых типов"]
        CS_NRT["Nullable-ссылочные типы<br/>string? name<br/>[WARNING] Только предупреждения компилятора"]
        
        CS_RUNTIME["Runtime NullReferenceException<br/>[ERROR] Всё ещё может упасть"]
        CS_NULL --> CS_RUNTIME
        CS_NRT -.-> CS_RUNTIME
        
        CS_CHECKS["Ручные проверки на null<br/>if (obj?.Property != null)"]
    end
    
    subgraph "Система Option<T> в Rust"
        RUST_OPTION["Option<T><br/>Some(value) | None"]
        RUST_FORCE["Компилятор заставляет обработать<br/>[OK] Нельзя проигнорировать None"]
        RUST_MATCH["Сопоставление с образцом<br/>match option { ... }"]
        RUST_METHODS["Богатый API<br/>.map(), .unwrap_or(), .and_then()"]
        
        RUST_OPTION --> RUST_FORCE
        RUST_FORCE --> RUST_MATCH
        RUST_FORCE --> RUST_METHODS
        
        RUST_SAFE["Null-безопасность на этапе компиляции<br/>[OK] Нет исключений null-указателя"]
        RUST_MATCH --> RUST_SAFE
        RUST_METHODS --> RUST_SAFE
    end
    
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000
    style CS_NRT fill:#fff3e0,color:#000
    style RUST_FORCE fill:#c8e6c9,color:#000

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn describe_point(point: Point) -> String {
    match point {
        Point { x: 0, y: 0 } => "начало координат".to_string(),
        Point { x: 0, y } => format!("на оси Y при y={}", y),
        Point { x, y: 0 } => format!("на оси X при x={}", x),
        Point { x, y } if x == y => format!("на диагонали в ({}, {})", x, y),
        Point { x, y } => format!("точка в ({}, {})", x, y),
    }
}
}

Option и Result

// Nullable-ссылочные типы C# (C# 8+)
public class PersonService
{
    private Dictionary<int, string> people = new();
    
    public string? FindPerson(int id)
    {
        return people.TryGetValue(id, out string? name) ? name : null;
    }
    
    public string GetPersonOrDefault(int id)
    {
        return FindPerson(id) ?? "Unknown";
    }
    
    // Обработка ошибок через исключения
    public void SavePerson(int id, string name)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentException("Name cannot be empty");
        
        people[id] = name;
    }
}
use std::collections::HashMap;

// Rust использует Option<T> вместо null
struct PersonService {
    people: HashMap<i32, String>,
}

impl PersonService {
    fn new() -> Self {
        PersonService {
            people: HashMap::new(),
        }
    }
    
    // Возвращает Option<T> — никакого null!
    fn find_person(&self, id: i32) -> Option<&String> {
        self.people.get(&id)
    }
    
    // Сопоставление с образцом по Option
    fn get_person_or_default(&self, id: i32) -> String {
        match self.find_person(id) {
            Some(name) => name.clone(),
            None => "Unknown".to_string(),
        }
    }
    
    // Использование методов Option (более функциональный стиль)
    fn get_person_or_default_functional(&self, id: i32) -> String {
        self.find_person(id)
            .map(|name| name.clone())
            .unwrap_or_else(|| "Unknown".to_string())
    }
    
    // Result<T, E> для обработки ошибок
    fn save_person(&mut self, id: i32, name: String) -> Result<(), String> {
        if name.is_empty() {
            return Err("Name cannot be empty".to_string());
        }
        
        self.people.insert(id, name);
        Ok(())
    }
    
    // Цепочка операций
    fn get_person_length(&self, id: i32) -> Option<usize> {
        self.find_person(id).map(|name| name.len())
    }
}

fn main() {
    let mut service = PersonService::new();
    
    // Обработка Result
    match service.save_person(1, "Alice".to_string()) {
        Ok(()) => println!("Person saved successfully"),
        Err(error) => println!("Error: {}", error),
    }
    
    // Обработка Option
    match service.find_person(1) {
        Some(name) => println!("Found: {}", name),
        None => println!("Person not found"),
    }
    
    // Функциональный стиль с Option
    let name_length = service.get_person_length(1)
        .unwrap_or(0);
    println!("Name length: {}", name_length);
    
    // Оператор вопросительного знака для раннего возврата
    fn try_operation(service: &mut PersonService) -> Result<String, String> {
        service.save_person(2, "Bob".to_string())?; // Ранний возврат при ошибке
        let name = service.find_person(2).ok_or("Person not found")?; // Преобразуем Option в Result
        Ok(format!("Hello, {}", name))
    }
    
    match try_operation(&mut service) {
        Ok(message) => println!("{}", message),
        Err(error) => println!("Operation failed: {}", error),
    }
}

Собственные типы ошибок

#![allow(unused)]
fn main() {
// Определяем собственное перечисление ошибок
#[derive(Debug)]
enum PersonError {
    NotFound(i32),
    InvalidName(String),
    DatabaseError(String),
}

impl std::fmt::Display for PersonError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PersonError::NotFound(id) => write!(f, "Person with ID {} not found", id),
            PersonError::InvalidName(name) => write!(f, "Invalid name: '{}'", name),
            PersonError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
        }
    }
}

impl std::error::Error for PersonError {}

// Расширенный PersonService с собственными ошибками
impl PersonService {
    fn save_person_enhanced(&mut self, id: i32, name: String) -> Result<(), PersonError> {
        if name.is_empty() || name.len() > 50 {
            return Err(PersonError::InvalidName(name));
        }
        
        // Имитация операции с базой данных, которая может завершиться ошибкой
        if id < 0 {
            return Err(PersonError::DatabaseError("Negative IDs not allowed".to_string()));
        }
        
        self.people.insert(id, name);
        Ok(())
    }
    
    fn find_person_enhanced(&self, id: i32) -> Result<&String, PersonError> {
        self.people.get(&id).ok_or(PersonError::NotFound(id))
    }
}

fn demo_error_handling() {
    let mut service = PersonService::new();
    
    // Обработка разных типов ошибок
    match service.save_person_enhanced(-1, "Invalid".to_string()) {
        Ok(()) => println!("Success"),
        Err(PersonError::NotFound(id)) => println!("Not found: {}", id),
        Err(PersonError::InvalidName(name)) => println!("Invalid name: {}", name),
        Err(PersonError::DatabaseError(msg)) => println!("DB Error: {}", msg),
    }
}
}

Упражнения

🏋️ Упражнение: комбинаторы Option (нажмите, чтобы раскрыть)

Перепишите этот глубоко вложенный C#-код с проверками на null, используя комбинаторы Option в Rust (and_then, map, unwrap_or):

string GetCityName(User? user)
{
    if (user != null)
        if (user.Address != null)
            if (user.Address.City != null)
                return user.Address.City.ToUpper();
    return "UNKNOWN";
}

Используйте такие типы Rust:

#![allow(unused)]
fn main() {
struct User { address: Option<Address> }
struct Address { city: Option<String> }
}

Запишите это как одно выражение без if let и match.

🔑 Решение
struct User { address: Option<Address> }
struct Address { city: Option<String> }

fn get_city_name(user: Option<&User>) -> String {
    user.and_then(|u| u.address.as_ref())
        .and_then(|a| a.city.as_ref())
        .map(|c| c.to_uppercase())
        .unwrap_or_else(|| "UNKNOWN".to_string())
}

fn main() {
    let user = User {
        address: Some(Address { city: Some("seattle".to_string()) }),
    };
    assert_eq!(get_city_name(Some(&user)), "SEATTLE");
    assert_eq!(get_city_name(None), "UNKNOWN");

    let no_city = User { address: Some(Address { city: None }) };
    assert_eq!(get_city_name(Some(&no_city)), "UNKNOWN");
}

Ключевая мысль: and_then — это аналог оператора ?. из Rust для Option. Каждый шаг возвращает Option, а цепочка прерывается на None — ровно как оператор null-условного доступа ?. в C#, но явно и с проверкой типов.


7. Владение и заимствование

Понимание владения

Что вы узнаете: систему владения Rust — почему let s2 = s1 делает s1 недействительным (в отличие от копирования ссылок в C#), три правила владения, типы Copy и Move, заимствование через & и &mut, а также то, как проверщик заимствований заменяет сборку мусора.

Сложность: 🟡 Средний

Владение — самая уникальная особенность Rust и самый большой концептуальный сдвиг для разработчиков C#. Разберём её шаг за шагом.

Модель памяти C# (повторение)

// C# — автоматическое управление памятью
public void ProcessData()
{
    var data = new List<int> { 1, 2, 3, 4, 5 };
    ProcessList(data);
    // data здесь по-прежнему доступен
    Console.WriteLine(data.Count);  // Работает нормально
    
    // GC очистит память, когда не останется ссылок
}

public void ProcessList(List<int> list)
{
    list.Add(6);  // Изменяет исходный список
}

Правила владения в Rust

  1. У каждого значения ровно один владелец (если вы не выберете разделяемое владение через Rc<T>/Arc<T> — см. Умные указатели)
  2. Когда владелец выходит из области видимости, значение уничтожается (детерминированная очистка — см. Drop)
  3. Владение можно передать (переместить)
#![allow(unused)]
fn main() {
// Rust — явное управление владением
fn process_data() {
    let data = vec![1, 2, 3, 4, 5];  // data владеет вектором
    process_list(data);              // Владение передано функции
    // println!("{:?}", data);       // ❌ Ошибка: data здесь больше не владеет значением
}

fn process_list(mut list: Vec<i32>) {  // list теперь владеет вектором
    list.push(6);
    // list уничтожается здесь, при завершении функции
}
}

Понятие «перемещения» для разработчиков C#

// C# — ссылки копируются, объекты остаются на месте
// (так работают только ссылочные типы — классы;
//  значимые типы C#, такие как struct, ведут себя иначе)
var original = new List<int> { 1, 2, 3 };
var reference = original;  // Обе переменные указывают на один объект
original.Add(4);
Console.WriteLine(reference.Count);  // 4 — тот же объект
#![allow(unused)]
fn main() {
// Rust — владение передаётся
let original = vec![1, 2, 3];
let moved = original;       // Владение передано
// println!("{:?}", original);  // ❌ Ошибка: original больше не владеет данными
println!("{:?}", moved);    // ✅ Работает: moved теперь владеет данными
}

Типы Copy и типы Move

#![allow(unused)]
fn main() {
// Типы Copy (как значимые типы в C#) — копируются, а не перемещаются
let x = 5;        // i32 реализует Copy
let y = x;        // x копируется в y
println!("{}", x); // ✅ Работает: x по-прежнему действителен

// Типы Move (как ссылочные типы в C#) — перемещаются, а не копируются
let s1 = String::from("hello");  // String не реализует Copy
let s2 = s1;                     // s1 перемещается в s2
// println!("{}", s1);           // ❌ Ошибка: s1 больше не действителен
}

Практический пример: обмен значениями

// C# — простой обмен ссылками
public void SwapLists(ref List<int> a, ref List<int> b)
{
    var temp = a;
    a = b;
    b = temp;
}
#![allow(unused)]
fn main() {
// Rust — обмен с учётом владения
fn swap_vectors(a: &mut Vec<i32>, b: &mut Vec<i32>) {
    std::mem::swap(a, b);  // Встроенная функция обмена
}

// Или ручной вариант
fn manual_swap() {
    let mut a = vec![1, 2, 3];
    let mut b = vec![4, 5, 6];
    
    let temp = a;  // Перемещаем a во временную переменную
    a = b;         // Перемещаем b в a
    b = temp;      // Перемещаем temp в b
    
    println!("a: {:?}, b: {:?}", a, b);
}
}

Основы заимствования

Заимствование похоже на получение ссылки в C#, но с гарантиями безопасности на этапе компиляции.

Ссылочные параметры в C#

// C# — параметры ref и out
public void ModifyValue(ref int value)
{
    value += 10;
}

public void ReadValue(in int value)  // ссылка только для чтения
{
    Console.WriteLine(value);
}

public bool TryParse(string input, out int result)
{
    return int.TryParse(input, out result);
}

Заимствование в Rust

// Rust — заимствование через & и &mut
fn modify_value(value: &mut i32) {  // Изменяемое заимствование
    *value += 10;
}

fn read_value(value: &i32) {        // Неизменяемое заимствование
    println!("{}", value);
}

fn main() {
    let mut x = 5;
    
    read_value(&x);      // Заимствуем неизменяемо
    modify_value(&mut x); // Заимствуем изменяемо
    
    println!("{}", x);   // x по-прежнему принадлежит нам
}

Правила заимствования (проверяются на этапе компиляции!)

#![allow(unused)]
fn main() {
fn borrowing_rules() {
    let mut data = vec![1, 2, 3];
    
    // Правило 1: несколько неизменяемых заимствований допустимы
    let r1 = &data;
    let r2 = &data;
    println!("{:?} {:?}", r1, r2);  // ✅ Работает
    
    // Правило 2: одновременно — только одно изменяемое заимствование
    let r3 = &mut data;
    // let r4 = &mut data;  // ❌ Ошибка: нельзя заимствовать изменяемо дважды
    // let r5 = &data;      // ❌ Ошибка: нельзя заимствовать неизменяемо, пока есть изменяемое заимствование
    
    r3.push(4);  // Используем изменяемое заимствование
    // r3 выходит из области видимости здесь
    
    // Правило 3: можно заимствовать снова после окончания предыдущих заимствований
    let r6 = &data;  // ✅ Теперь работает
    println!("{:?}", r6);
}
}

C# против Rust: безопасность ссылок

// C# — возможны ошибки времени выполнения
public class ReferenceSafety
{
    private List<int> data = new List<int>();
    
    public List<int> GetData() => data;  // Возвращает ссылку на внутреннее состояние
    
    public void UnsafeExample()
    {
        var reference = GetData();
        
        // Другой поток может изменить data прямо здесь!
        Thread.Sleep(1000);
        
        // reference может быть недействительным или изменённым
        reference.Add(42);  // Возможное состояние гонки
    }
}
#![allow(unused)]
fn main() {
// Rust — безопасность на этапе компиляции
pub struct SafeContainer {
    data: Vec<i32>,
}

impl SafeContainer {
    // Возвращаем неизменяемое заимствование — вызывающий код не может изменить данные
    // Предпочитайте &[i32] вместо &Vec<i32> — принимайте самый широкий тип
    pub fn get_data(&self) -> &[i32] {
        &self.data
    }
    
    // Возвращаем изменяемое заимствование — эксклюзивный доступ гарантирован
    pub fn get_data_mut(&mut self) -> &mut Vec<i32> {
        &mut self.data
    }
}

fn safe_example() {
    let mut container = SafeContainer { data: vec![1, 2, 3] };
    
    let reference = container.get_data();
    // container.get_data_mut();  // ❌ Ошибка: нельзя заимствовать изменяемо, пока есть неизменяемое заимствование
    
    println!("{:?}", reference);  // Используем неизменяемую ссылку
    // reference выходит из области видимости здесь
    
    let mut_reference = container.get_data_mut();  // ✅ Теперь можно
    mut_reference.push(4);
}
}

Семантика перемещения

Значимые и ссылочные типы в C#

// C# — значимые типы копируются
struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

var p1 = new Point { X = 1, Y = 2 };
var p2 = p1;  // Копирование
p2.X = 10;
Console.WriteLine(p1.X);  // По-прежнему 1

// C# — ссылочные типы разделяют объект
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;  // Копирование ссылки
list2.Add(4);
Console.WriteLine(list1.Count);  // 4 — тот же объект

Семантика перемещения в Rust

#![allow(unused)]
fn main() {
// Rust — по умолчанию перемещение для типов, не реализующих Copy
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn move_example() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;  // Перемещение (не копирование)
    // println!("{:?}", p1);  // ❌ Ошибка: p1 перемещён
    println!("{:?}", p2);    // ✅ Работает
}

// Чтобы включить копирование, реализуйте трейт Copy
#[derive(Debug, Copy, Clone)]
struct CopyablePoint {
    x: i32,
    y: i32,
}

fn copy_example() {
    let p1 = CopyablePoint { x: 1, y: 2 };
    let p2 = p1;  // Копирование (потому что реализован Copy)
    println!("{:?}", p1);  // ✅ Работает
    println!("{:?}", p2);  // ✅ Работает
}
}

Когда значения перемещаются

#![allow(unused)]
fn main() {
fn demonstrate_moves() {
    let s = String::from("hello");
    
    // 1. Присваивание перемещает
    let s2 = s;  // s перемещается в s2
    
    // 2. Вызовы функций перемещают
    take_ownership(s2);  // s2 перемещается в функцию
    
    // 3. Возврат из функций перемещает
    let s3 = give_ownership();  // Возвращаемое значение перемещается в s3
    
    println!("{}", s3);  // s3 действителен
}

fn take_ownership(s: String) {
    println!("{}", s);
    // s уничтожается здесь
}

fn give_ownership() -> String {
    String::from("yours")  // Владение перемещается к вызывающему коду
}
}

Избегаем перемещений с помощью заимствования

#![allow(unused)]
fn main() {
fn demonstrate_borrowing() {
    let s = String::from("hello");
    
    // Заимствуем вместо перемещения
    let len = calculate_length(&s);  // s заимствуется
    println!("'{}' has length {}", s, len);  // s по-прежнему действителен
}

fn calculate_length(s: &String) -> usize {
    s.len()  // s не принадлежит нам, поэтому не уничтожается
}
}

Управление памятью: GC против RAII

Сборка мусора в C#

// C# — автоматическое управление памятью
public class Person
{
    public string Name { get; set; }
    public List<string> Hobbies { get; set; } = new List<string>();
    
    public void AddHobby(string hobby)
    {
        Hobbies.Add(hobby);  // Память выделяется автоматически
    }
    
    // Явная очистка не нужна — GC с ней справляется
    // Но для ресурсов существует паттерн IDisposable
}

using var file = new FileStream("data.txt", FileMode.Open);
// 'using' гарантирует вызов Dispose()

Владение и RAII в Rust

#![allow(unused)]
fn main() {
// Rust — управление памятью на этапе компиляции
pub struct Person {
    name: String,
    hobbies: Vec<String>,
}

impl Person {
    pub fn add_hobby(&mut self, hobby: String) {
        self.hobbies.push(hobby);  // Управление памятью отслеживается на этапе компиляции
    }
    
        // Трейт Drop реализуется автоматически — очистка гарантирована
    // Сравните с IDisposable в C#:
    //   C#:   using var file = new FileStream(...)    // Dispose() вызывается в конце блока using
    //   Rust: let file = File::open(...)?             // drop() вызывается в конце области видимости — 'using' не нужен
}

// RAII — Resource Acquisition Is Initialization (получение ресурса есть инициализация)
{
    let file = std::fs::File::open("data.txt")?;
    // Файл автоматически закрывается, когда 'file' выходит из области видимости
    // Оператор using не нужен — это обеспечивает система типов
}
}
graph TD
    subgraph "Управление памятью в C#"
        CS_ALLOC["Выделение объекта<br/>new Person()"]
        CS_HEAP["Управляемая куча"]
        CS_REF["Ссылки указывают на кучу"]
        CS_GC_CHECK["GC периодически проверяет<br/>недостижимые объекты"]
        CS_SWEEP["Сборка по принципу<br/>mark and sweep"]
        CS_PAUSE["[ERROR] Паузы GC"]
        
        CS_ALLOC --> CS_HEAP
        CS_HEAP --> CS_REF
        CS_REF --> CS_GC_CHECK
        CS_GC_CHECK --> CS_SWEEP
        CS_SWEEP --> CS_PAUSE
        
        CS_ISSUES["[ERROR] Недетерминированная очистка<br/>[ERROR] Давление на память<br/>[ERROR] Сложность финализации<br/>[OK] Легко использовать"]
    end
    
    subgraph "Система владения Rust"
        RUST_ALLOC["Создание значения<br/>Person { ... }"]
        RUST_OWNER["Единственный владелец<br/>на стеке или в куче"]
        RUST_BORROW["Система заимствований<br/>&T, &mut T"]
        RUST_SCOPE["Очистка по области видимости<br/>трейт Drop"]
        RUST_COMPILE["Проверка на этапе компиляции"]
        
        RUST_ALLOC --> RUST_OWNER
        RUST_OWNER --> RUST_BORROW
        RUST_BORROW --> RUST_SCOPE
        RUST_SCOPE --> RUST_COMPILE
        
        RUST_BENEFITS["[OK] Детерминированная очистка<br/>[OK] Нулевые накладные расходы рантайма<br/>[OK] Никаких утечек памяти<br/>[ERROR] Кривая обучения"]
    end
    
    style CS_ISSUES fill:#ffebee,color:#000
    style RUST_BENEFITS fill:#e8f5e8,color:#000
    style CS_PAUSE fill:#ffcdd2,color:#000
    style RUST_COMPILE fill:#c8e6c9,color:#000

🏋️ Упражнение: исправьте ошибки проверщика заимствований (нажмите, чтобы раскрыть)

Задача: в каждом фрагменте ниже есть ошибка проверщика заимствований. Исправьте их, не меняя вывод.

#![allow(unused)]
fn main() {
// 1. Перемещение после использования
fn problem_1() {
    let name = String::from("Alice");
    let greeting = format!("Hello, {name}!");
    let upper = name.to_uppercase();  // подсказка: заимствуйте вместо перемещения
    println!("{greeting} — {upper}");
}

// 2. Пересечение изменяемого и неизменяемого заимствований
fn problem_2() {
    let mut numbers = vec![1, 2, 3];
    let first = &numbers[0];
    numbers.push(4);            // подсказка: переупорядочьте операции
    println!("first = {first}");
}

// 3. Возврат ссылки на локальную переменную
fn problem_3() -> String {
    let s = String::from("hello");
    s   // подсказка: верните владеемое значение, а не &str
}
}
🔑 Решение
#![allow(unused)]
fn main() {
// 1. format! уже заимствует — исправление в том, что format! принимает ссылку.
//    Исходный код на самом деле компилируется! Но если бы было `let greeting = name;`,
//    исправили бы через &name:
fn solution_1() {
    let name = String::from("Alice");
    let greeting = format!("Hello, {}!", &name); // заимствование
    let upper = name.to_uppercase();             // name по-прежнему действителен
    println!("{greeting} — {upper}");
}

// 2. Используем неизменяемое заимствование до изменяющей операции:
fn solution_2() {
    let mut numbers = vec![1, 2, 3];
    let first = numbers[0]; // копируем значение i32 (i32 реализует Copy)
    numbers.push(4);
    println!("first = {first}");
}

// 3. Возвращаем владеемую String (уже правильно — частая путаница у новичков):
fn solution_3() -> String {
    let s = String::from("hello");
    s // владение передаётся вызывающему коду — это правильный паттерн
}
}

Ключевые выводы:

  • format!() заимствует свои аргументы — он их не перемещает
  • Примитивные типы вроде i32 реализуют Copy, поэтому индексация копирует значение
  • Возврат владеемого значения передаёт владение вызывающему коду — проблем с временами жизни нет

Глубокое погружение в безопасность памяти

Ссылки против указателей

Что вы узнаете: ссылки Rust против указателей и небезопасных контекстов C#, основы времён жизни, а также почему доказательства безопасности на этапе компиляции надёжнее проверок времени выполнения в C# (проверка границ, защита от null).

Сложность: 🟡 Средний

Указатели в C# (небезопасный контекст)

// Небезопасные указатели в C# (используются редко)
unsafe void UnsafeExample()
{
    int value = 42;
    int* ptr = &value;  // Указатель на value
    *ptr = 100;         // Разыменование и изменение
    Console.WriteLine(value);  // 100
}

Ссылки в Rust (безопасны по умолчанию)

#![allow(unused)]
fn main() {
// Ссылки в Rust (всегда безопасны)
fn safe_example() {
    let mut value = 42;
    let ptr = &mut value;  // Изменяемая ссылка
    *ptr = 100;           // Разыменование и изменение
    println!("{}", value); // 100
}

// Ключевое слово "unsafe" не нужно — проверщик заимствований гарантирует безопасность
}

Основы времён жизни для разработчиков C#

// C# — можно вернуть ссылки, которые могут стать недействительными
public class LifetimeIssues
{
    public string GetFirstWord(string input)
    {
        return input.Split(' ')[0];  // Возвращает новую строку (безопасно)
    }
    
    public unsafe char* GetFirstChar(string input)
    {
        // Это было бы опасно — возврат указателя на управляемую память
        fixed (char* ptr = input)
            return ptr;  // ❌ Плохо: ptr становится недействительным после выхода из метода
    }
}
#![allow(unused)]
fn main() {
// Rust — проверка времён жизни предотвращает висячие ссылки
fn get_first_word(input: &str) -> &str {
    input.split_whitespace().next().unwrap_or("")
    // ✅ Безопасно: возвращаемая ссылка имеет то же время жизни, что и input
}

fn invalid_reference() -> &str {
    let temp = String::from("hello");
    &temp  // ❌ Ошибка компиляции: temp живёт недостаточно долго
    // temp будет уничтожена в конце функции
}

fn valid_reference() -> String {
    let temp = String::from("hello");
    temp  // ✅ Работает: владение передаётся вызывающему коду
}
}

Безопасность памяти: проверки во время выполнения против доказательств на этапе компиляции

C# — страховка времени выполнения

// C# полагается на проверки во время выполнения и GC
public class Buffer
{
    private byte[] data;
    
    public Buffer(int size)
    {
        data = new byte[size];
    }
    
    public void ProcessData(int index)
    {
        // Проверка границ во время выполнения
        if (index >= data.Length)
            throw new IndexOutOfRangeException();
            
        data[index] = 42;  // Безопасно, но проверяется во время выполнения
    }
    
    // Утечки памяти всё равно возможны через события и статические ссылки
    public static event Action<string> GlobalEvent;
    
    public void Subscribe()
    {
        GlobalEvent += HandleEvent;  // Может создать утечку памяти
        // Забыли отписаться — объект не будет собран сборщиком мусора
    }
    
    private void HandleEvent(string message) { /* ... */ }
    
    // Исключения NullReferenceException по-прежнему возможны
    public void ProcessUser(User user)
    {
        Console.WriteLine(user.Name.ToUpper());  // NullReferenceException, если user.Name равен null
    }
    
    // Доступ к массиву может завершиться ошибкой во время выполнения
    public int GetValue(int[] array, int index)
    {
        return array[index];  // Возможно IndexOutOfRangeException
    }
}

Rust — гарантии на этапе компиляции

#![allow(unused)]
fn main() {
struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    fn new(size: usize) -> Self {
        Buffer {
            data: vec![0; size],
        }
    }
    
    fn process_data(&mut self, index: usize) {
        // Проверку границ компилятор может убрать, если безопасность доказана
        if let Some(item) = self.data.get_mut(index) {
            *item = 42;  // Безопасный доступ, доказанный на этапе компиляции
        }
        // Или используйте индексирование с явной проверкой границ:
        // self.data[index] = 42;  // Паника в debug-сборке, но память в безопасности
    }
    
    // Утечки памяти невозможны — система владения их предотвращает
    fn process_with_closure<F>(&mut self, processor: F) 
    where F: FnOnce(&mut Vec<u8>)
    {
        processor(&mut self.data);
        // Когда processor выходит из области видимости, он автоматически очищается
        // Создать висячие ссылки или утечку памяти нельзя
    }
    
    // Разыменование null-указателей невозможно — null-указателей здесь нет!
    fn process_user(&self, user: &User) {
        println!("{}", user.name.to_uppercase());  // user.name не может быть null
    }
    
    // Доступ к массиву либо проверяется на границы, либо явно небезопасен
    fn get_value(array: &[i32], index: usize) -> Option<i32> {
        array.get(index).copied()  // Возвращает None, если индекс вне границ
    }
    
    // Или явно небезопасно, если вы точно знаете, что делаете:
    /// # Safety
    /// `index` должен быть меньше `array.len()`.
    unsafe fn get_value_unchecked(array: &[i32], index: usize) -> i32 {
        *array.get_unchecked(index)  // Быстро, но границы нужно доказать вручную
    }
}

struct User {
    name: String,  // String в Rust не может быть null
}

// Владение предотвращает использование после освобождения
fn ownership_example() {
    let data = vec![1, 2, 3, 4, 5];
    let reference = &data[0];  // Заимствуем data
    
    // drop(data);  // ОШИБКА: нельзя уничтожить, пока есть заимствование
    println!("{}", reference);  // Это гарантированно безопасно
}

// Заимствование предотвращает гонки данных
fn borrowing_example(data: &mut Vec<i32>) {
    let first = &data[0];  // Неизменяемое заимствование
    // data.push(6);  // ОШИБКА: нельзя заимствовать изменяемо, пока есть неизменяемое заимствование
    println!("{}", first);  // Гарантированно нет гонки данных
}
}
graph TD
    subgraph "Безопасность во время выполнения в C#"
        CS_RUNTIME["Проверки во время выполнения"]
        CS_GC["Сборщик мусора"]
        CS_EXCEPTIONS["Обработка исключений"]
        CS_BOUNDS["Проверка границ во время выполнения"]
        CS_NULL["Исключения null-ссылок"]
        CS_LEAKS["Возможны утечки памяти"]
        CS_OVERHEAD["Накладные расходы производительности"]
        
        CS_RUNTIME --> CS_BOUNDS
        CS_RUNTIME --> CS_NULL
        CS_GC --> CS_LEAKS
        CS_EXCEPTIONS --> CS_OVERHEAD
    end
    
    subgraph "Безопасность на этапе компиляции в Rust"
        RUST_OWNERSHIP["Система владения"]
        RUST_BORROWING["Проверщик заимствований"]
        RUST_TYPES["Система типов"]
        RUST_ZERO_COST["Абстракции с нулевой стоимостью"]
        RUST_NO_NULL["Нет null-указателей"]
        RUST_NO_LEAKS["Нет утечек памяти"]
        RUST_FAST["Оптимальная производительность"]
        
        RUST_OWNERSHIP --> RUST_NO_LEAKS
        RUST_BORROWING --> RUST_NO_NULL
        RUST_TYPES --> RUST_ZERO_COST
        RUST_ZERO_COST --> RUST_FAST
    end
    
    style CS_NULL fill:#ffcdd2,color:#000
    style CS_LEAKS fill:#ffcdd2,color:#000
    style CS_OVERHEAD fill:#fff3e0,color:#000
    style RUST_NO_NULL fill:#c8e6c9,color:#000
    style RUST_NO_LEAKS fill:#c8e6c9,color:#000
    style RUST_FAST fill:#c8e6c9,color:#000

Упражнения

🏋️ Упражнение: найдите ошибку безопасности (нажмите, чтобы раскрыть)

В этом C#-коде есть тонкая ошибка безопасности. Найдите её, затем напишите эквивалент на Rust и объясните, почему версия на Rust не скомпилируется:

public List<int> GetEvenNumbers(List<int> numbers)
{
    var result = new List<int>();
    foreach (var n in numbers)
    {
        if (n % 2 == 0)
        {
            result.Add(n);
            numbers.Remove(n);  // Ошибка: изменение коллекции во время перебора
        }
    }
    return result;
}
🔑 Решение

Ошибка в C#: изменение numbers во время перебора приводит к InvalidOperationException во время выполнения. Её легко пропустить на код-ревью.

fn get_even_numbers(numbers: &mut Vec<i32>) -> Vec<i32> {
    let mut result = Vec::new();
    for &n in numbers.iter() {
        if n % 2 == 0 {
            result.push(n);
            // numbers.retain(|&x| x != n);
            // ❌ ОШИБКА: нельзя заимствовать `*numbers` как изменяемое, потому что
            //    оно уже заимствовано как неизменяемое (итератором)
        }
    }
    result
}

// Идиоматичный Rust: используйте retain или partition
fn get_even_numbers_idiomatic(numbers: &mut Vec<i32>) -> Vec<i32> {
    let evens: Vec<i32> = numbers.iter().copied().filter(|n| n % 2 == 0).collect();
    numbers.retain(|n| n % 2 != 0); // удаляем чётные после перебора
    evens
}

fn main() {
    let mut nums = vec![1, 2, 3, 4, 5, 6];
    let evens = get_even_numbers_idiomatic(&mut nums);
    assert_eq!(evens, vec![2, 4, 6]);
    assert_eq!(nums, vec![1, 3, 5]);
}

Ключевая мысль: проверщик заимствований Rust предотвращает целую категорию ошибок «изменение во время перебора» на этапе компиляции. C# ловит это во время выполнения; многие языки не ловят вообще.


Глубокое погружение во времена жизни

Времена жизни: как сообщить компилятору, сколько живут ссылки

Что вы узнаете: зачем нужны времена жизни (без GC компилятору нужно доказательство), синтаксис аннотаций времён жизни, правила элизии, времена жизни в структурах, время жизни 'static, а также типичные ошибки проверщика заимствований и способы их исправить.

Сложность: 🔴 Продвинутый

Разработчики C# никогда не думают о временах жизни ссылок — достижимостью занимается сборщик мусора. В Rust компилятору нужно доказательство того, что каждая ссылка действительна столько, сколько она используется. Времена жизни и есть это доказательство.

Зачем нужны времена жизни

#![allow(unused)]
fn main() {
// Этот код не скомпилируется — компилятор не может доказать, что возвращаемая ссылка действительна
fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
// ОШИБКА: пропущен specifier времени жизни — компилятор не знает,
// заимствует ли возвращаемое значение из `a` или из `b`
}

Аннотации времён жизни

// Время жизни 'a означает: «возвращаемое значение живёт не меньше, чем ОБА входных параметра»
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let result;
    let string1 = String::from("long string");
    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
        println!("Longest: {result}"); // ✅ обе ссылки здесь ещё действительны
    }
    // println!("{result}"); // ❌ ОШИБКА: string2 живёт недостаточно долго
}

Сравнение с C#

// C# — GC держит объекты живыми, пока существует хотя бы одна ссылка
string Longest(string a, string b) => a.Length > b.Length ? a : b;

// Проблем с временами жизни нет — GC сам отслеживает достижимость
// Но: паузы GC, непредсказуемое потребление памяти, нет доказательств на этапе компиляции

Правила элизии времён жизни

В большинстве случаев аннотации времён жизни писать не нужно. Компилятор применяет три правила автоматически:

ПравилоОписаниеПример
Правило 1Каждый параметр-ссылка получает собственное время жизниfn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str)
Правило 2Если есть ровно одно входное время жизни, оно присваивается всем выходнымfn first(s: &str) -> &str → fn first<'a>(s: &'a str) -> &'a str
Правило 3Если один из входов — &self или &mut self, его время жизни присваивается всем выходнымfn name(&self) -> &str → работает благодаря &self
#![allow(unused)]
fn main() {
// Эти записи эквивалентны — компилятор добавляет времена жизни автоматически:
fn first_word(s: &str) -> &str { /* ... */ }           // с элизией
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } // явно

// А вот здесь нужна явная аннотация — два входа, из какого из них заимствует выход?
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ }
}

Времена жизни в структурах

// Структура, которая заимствует данные (а не владеет ими)
struct Excerpt<'a> {
    text: &'a str,  // заимствует из String, которая должна пережить эту структуру
}

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        Excerpt { text }
    }

    fn first_sentence(&self) -> &str {
        self.text.split('.').next().unwrap_or(self.text)
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel); // excerpt заимствует из novel
    println!("First sentence: {}", excerpt.first_sentence());
    // novel должен жить, пока существует excerpt
}
// Аналог в C# — проблем с временами жизни нет, но и гарантий на этапе компиляции нет
class Excerpt
{
    public string Text { get; }
    public Excerpt(string text) => Text = text;
    public string FirstSentence() => Text.Split('.')[0];
}
// Что если строку изменят где-то в другом месте? Сюрприз во время выполнения.

Время жизни 'static

#![allow(unused)]
fn main() {
// 'static означает «живёт всё время работы программы»
let s: &'static str = "I'm a string literal"; // хранится в бинарнике, всегда действителен

// Типичные места, где встречается 'static:
// 1. Строковые литералы
// 2. Глобальные константы
// 3. Thread::spawn требует 'static (поток может пережить вызывающий код)
std::thread::spawn(move || {
    // Замыкания, передаваемые в потоки, должны владеть своими данными или использовать ссылки 'static
    println!("{s}"); // OK: &'static str
});

// 'static НЕ означает «бессмертный» — это значит «МОЖЕТ жить вечно, если потребуется»
let owned = String::from("hello");
// owned не имеет времени жизни 'static, но его можно переместить в поток (передача владения)
}

Типичные ошибки проверщика заимствований и их исправление

ОшибкаПричинаИсправление
missing lifetime specifierНесколько входных ссылок, неоднозначный выходДобавьте <'a>, связав выход с нужным входом
does not live long enoughСсылка переживает данные, на которые указываетРасширьте область видимости данных или верните владеемые данные
cannot borrow as mutableНеизменяемое заимствование ещё активноИспользуйте неизменяемую ссылку до изменения или перестройте код
cannot move out of borrowed contentПопытка забрать владение заимствованными даннымиИспользуйте .clone() или перестройте код, чтобы избежать перемещения
lifetime may not live long enoughЗаимствование структуры переживает источникУбедитесь, что область видимости исходных данных охватывает использование структуры

Визуализация областей видимости времён жизни

graph TD
    subgraph "Визуализация областей видимости"
        direction TB
        A["fn main()"] --> B["let s1 = String::from(&quot;hello&quot;)"]
        B --> C["{ // внутренняя область"]
        C --> D["let s2 = String::from(&quot;world&quot;)"]
        D --> E["let r = longest(&s1, &s2)"]
        E --> F["println!(&quot;{r}&quot;)  ✅ обе живы"]
        F --> G["} // s2 уничтожается здесь"]
        G --> H["println!(&quot;{r}&quot;)  ❌ s2 уже нет!"]
    end

    style F fill:#c8e6c9,color:#000
    style H fill:#ffcdd2,color:#000

Несколько параметров времени жизни

Иногда ссылки приходят из разных источников с разными временами жизни:

// Два независимых времени жизни: результат заимствует только из 'a, а не из 'b
fn first_with_context<'a, 'b>(data: &'a str, _context: &'b str) -> &'a str {
    // Результат заимствуется только из 'data' — у 'context' может быть более короткое время жизни
    data.split(',').next().unwrap_or(data)
}

fn main() {
    let data = String::from("alice,bob,charlie");
    let result;
    {
        let context = String::from("user lookup"); // более короткое время жизни
        result = first_with_context(&data, &context);
    } // context уничтожается — но result заимствует из data, а не из context ✅
    println!("{result}");
}
// C# — без отслеживания времён жизни нельзя выразить «заимствует из A, но не из B»
string FirstWithContext(string data, string context) => data.Split(',')[0];
// Для языков с GC это нормально, но Rust может доказать безопасность без GC

Типичные паттерны из практики

Паттерн 1: итератор, возвращающий ссылки

#![allow(unused)]
fn main() {
// Парсер, который отдаёт заимствованные срезы из входной строки
struct CsvRow<'a> {
    fields: Vec<&'a str>,
}

fn parse_csv_line(line: &str) -> CsvRow<'_> {
    // '_ говорит компилятору «выведи время жизни из входного параметра»
    CsvRow {
        fields: line.split(',').collect(),
    }
}
}

Паттерн 2: «если сомневаешься — возвращай владеемые данные»

#![allow(unused)]
fn main() {
// Когда времена жизни становятся сложными, практичное решение — вернуть владеемые данные
fn format_greeting(first: &str, last: &str) -> String {
    // Возвращаем владеемую String — аннотация времени жизни не нужна
    format!("Hello, {first} {last}!")
}

// Заимствуйте только тогда, когда:
// 1. Важна производительность (избегаем выделения памяти)
// 2. Связь между временем жизни входа и выхода очевидна
}

Паттерн 3: ограничения времён жизни для обобщений

#![allow(unused)]
fn main() {
// «T должен жить не меньше, чем 'a»
fn store_reference<'a, T: 'a>(cache: &mut Vec<&'a T>, item: &'a T) {
    cache.push(item);
}

// Часто встречается в трейт-объектах: Box<dyn Display + 'a>
fn make_printer<'a>(text: &'a str) -> Box<dyn std::fmt::Display + 'a> {
    Box::new(text)
}
}

Когда использовать 'static

СценарийИспользовать 'static?Альтернатива
Строковые литералы✅ Да — они всегда 'static—
Замыкание для thread::spawnЧасто — поток переживает вызывающий кодИспользуйте thread::scope для заимствованных данных
Глобальная конфигурация✅ lazy_static! или OnceLockПередавайте ссылки через параметры
Трейт-объекты, которые хранятся долгоЧасто — Box<dyn Trait + 'static>Параметризуйте контейнер через 'a
Временные заимствования❌ Никогда — излишнее ограничениеИспользуйте реальное время жизни
🏋️ Упражнение: аннотации времён жизни (нажмите, чтобы раскрыть)

Задача: добавьте правильные аннотации времён жизни, чтобы код скомпилировался:

#![allow(unused)]
fn main() {
struct Config {
    db_url: String,
    api_key: String,
}

// TODO: Добавьте аннотации времён жизни
fn get_connection_info(config: &Config) -> (&str, &str) {
    (&config.db_url, &config.api_key)
}

// TODO: Эта структура заимствует из Config — добавьте параметр времени жизни
struct ConnectionInfo {
    db_url: &str,
    api_key: &str,
}
}
🔑 Решение
#![allow(unused)]
fn main() {
struct Config {
    db_url: String,
    api_key: String,
}

// Правило 3 не применяется (нет &self), применяется правило 2 (один вход → выход)
// Поэтому компилятор справляется сам — аннотация не нужна!
fn get_connection_info(config: &Config) -> (&str, &str) {
    (&config.db_url, &config.api_key)
}

// Аннотация времени жизни для структуры нужна:
struct ConnectionInfo<'a> {
    db_url: &'a str,
    api_key: &'a str,
}

fn make_info<'a>(config: &'a Config) -> ConnectionInfo<'a> {
    ConnectionInfo {
        db_url: &config.db_url,
        api_key: &config.api_key,
    }
}
}

Ключевая мысль: элизия времён жизни часто избавляет от необходимости писать аннотации у функций, но структуры, которые заимствуют данные, всегда требуют явного <'a>.


Умные указатели — не только единоличное владение

Умные указатели: когда единоличного владения недостаточно

Что вы узнаете: Box<T>, Rc<T>, Arc<T>, Cell<T>, RefCell<T> и Cow<'a, T> — когда что использовать, как они соотносятся со ссылками, которыми управляет GC в C#, Drop как аналог IDisposable в Rust, приведение через Deref, а также дерево решений для выбора подходящего умного указателя.

Сложность: 🔴 Продвинутый

В C# каждый объект по сути подсчитывается сборщиком мусора. В Rust по умолчанию действует единоличное владение, но иногда нужны разделяемое владение, выделение в куче или внутренняя изменяемость. Для этого и существуют умные указатели.

Box<T> — простое выделение в куче

#![allow(unused)]
fn main() {
// Выделение на стеке (по умолчанию в Rust)
let x = 42;           // на стеке

// Выделение в куче через Box
let y = Box::new(42); // в куче, как `new int(42)` в C# (упаковка)
println!("{}", y);     // автоматическое разыменование: выводит 42

// Типичное применение: рекурсивные типы (размер неизвестен на этапе компиляции)
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),  // Box даёт указатель известного размера
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
// C# — всё уже в куче (ссылочные типы)
// Box<T> нужен в Rust только потому, что стек — это умолчание
var list = new LinkedListNode<int>(1);  // всегда выделяется в куче

Rc<T> — разделяемое владение (один поток)

#![allow(unused)]
fn main() {
use std::rc::Rc;

// Несколько владельцев одних и тех же данных — как несколько ссылок в C#
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared); // счётчик ссылок: 2
let clone2 = Rc::clone(&shared); // счётчик ссылок: 3

println!("Count: {}", Rc::strong_count(&shared)); // 3
// Данные уничтожаются, когда выходит из области видимости последний Rc

// Типичное применение: общая конфигурация, узлы графа, древовидные структуры
}

Arc<T> — разделяемое владение (потокобезопасное)

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

// Arc = Atomic Reference Counting — безопасно разделяется между потоками
let data = Arc::new(vec![1, 2, 3]);

let handles: Vec<_> = (0..3).map(|i| {
    let data = Arc::clone(&data);
    thread::spawn(move || {
        println!("Thread {i}: {:?}", data);
    })
}).collect();

for h in handles { h.join().unwrap(); }
}
// C# — все ссылки потокобезопасны по умолчанию (этим занимается GC)
var data = new List<int> { 1, 2, 3 };
// Можно свободно разделять между потоками (но мутация по-прежнему небезопасна!)

Cell<T> и RefCell<T> — внутренняя изменяемость

#![allow(unused)]
fn main() {
use std::cell::RefCell;

// Иногда нужно изменять данные за разделяемой ссылкой.
// RefCell переносит проверку заимствований из времени компиляции во время выполнения.
struct Logger {
    entries: RefCell<Vec<String>>,
}

impl Logger {
    fn new() -> Self {
        Logger { entries: RefCell::new(Vec::new()) }
    }

    fn log(&self, msg: &str) { // &self, а не &mut self!
        self.entries.borrow_mut().push(msg.to_string());
    }

    fn dump(&self) {
        for entry in self.entries.borrow().iter() {
            println!("{entry}");
        }
    }
}
// ⚠️ RefCell вызывает панику во время выполнения, если нарушены правила заимствования
// Используйте осторожно — по возможности предпочитайте проверку на этапе компиляции
}

Cow<’a, str> — копирование при записи (Clone on Write)

#![allow(unused)]
fn main() {
use std::borrow::Cow;

// Иногда есть &str, который МОЖЕТ понадобиться превратить в String
fn normalize(input: &str) -> Cow<'_, str> {
    if input.contains('\t') {
        // Выделяем память, только когда нужно изменить данные
        Cow::Owned(input.replace('\t', "    "))
    } else {
        // Заимствуем исходные данные — без выделения памяти
        Cow::Borrowed(input)
    }
}

let clean = normalize("hello");           // Cow::Borrowed — без выделения памяти
let dirty = normalize("hello\tworld");    // Cow::Owned — выделено
// Оба можно использовать как &str через Deref
println!("{clean} / {dirty}");
}

Drop: аналог IDisposable в Rust

В C# очисткой ресурсов занимаются IDisposable и using. Аналог в Rust — трейт Drop, но он автоматический, а не включаемый по желанию:

// C# — нужно не забыть использовать 'using' или вызвать Dispose()
using var file = File.OpenRead("data.bin");
// Dispose() вызывается в конце области видимости

// Забыть 'using' — значит получить утечку ресурса!
var file2 = File.OpenRead("data.bin");
// GC в итоге финализирует объект, но момент непредсказуем
// Rust — Drop выполняется автоматически, когда значение выходит из области видимости
{
    let file = File::open("data.bin")?;
    // работаем с file...
}   // file.drop() вызывается ЗДЕСЬ, детерминированно — 'using' не нужен

// Собственный Drop (аналог реализации IDisposable)
struct TempFile {
    path: std::path::PathBuf,
}

impl Drop for TempFile {
    fn drop(&mut self) {
        // Гарантированно выполняется, когда TempFile выходит из области видимости
        let _ = std::fs::remove_file(&self.path);
        println!("Cleaned up {:?}", self.path);
    }
}

fn main() {
    let tmp = TempFile { path: "scratch.tmp".into() };
    // ... используем tmp ...
}   // scratch.tmp удаляется автоматически здесь

Ключевое отличие от C#: в Rust любой тип может иметь детерминированную очистку. Вы никогда не забудете using, потому что забывать нечего — Drop выполняется, когда владелец выходит из области видимости. Этот паттерн называется RAII (Resource Acquisition Is Initialization — получение ресурса есть инициализация).

Правило: если ваш тип хранит ресурс (дескриптор файла, сетевое соединение, охранник блокировки, временный файл), реализуйте Drop. Система владения гарантирует, что он выполнится ровно один раз.

Приведение через Deref: автоматическое разворачивание умных указателей

Rust автоматически «разворачивает» умные указатели, когда вы вызываете методы или передаёте их в функции. Это называется приведением через Deref (Deref coercion):

#![allow(unused)]
fn main() {
let boxed: Box<String> = Box::new(String::from("hello"));

// Цепочка приведения через Deref: Box<String> → String → str
println!("Length: {}", boxed.len());   // вызывается str::len() — автоматическое разыменование!

fn greet(name: &str) {
    println!("Hello, {name}");
}

let s = String::from("Alice");
greet(&s);       // &String → &str через приведение Deref
greet(&boxed);   // &Box<String> → &String → &str — два уровня!
}
// В C# аналога нет — понадобились бы явные приведения или .ToString()
// Ближайшее — неявные операторы преобразования, но их нужно явно определить

Почему это важно: вы можете передать &String там, где ожидается &str, &Vec<T> там, где ожидается &[T], и &Box<T> там, где ожидается &T, — и всё это без явного преобразования. Поэтому API на Rust обычно принимают &str и &[T], а не &String и &Vec<T>.

Rc против Arc: что выбрать

Rc<T>Arc<T>
Потокобезопасность❌ Только один поток✅ Потокобезопасен (атомарные операции)
Накладные расходыНиже (неатомарный счётчик ссылок)Выше (атомарный счётчик ссылок)
Проверка компиляторомНе скомпилируется при передаче в thread::spawnРаботает везде
Сочетается сRefCell<T> для измененияMutex<T> или RwLock<T> для изменения

Эмпирическое правило: начинайте с Rc. Компилятор подскажет, если понадобится Arc.

Дерево решений: какой умный указатель выбрать?

graph TD
    START["Нужно разделяемое владение<br/>или выделение в куче?"]
    HEAP["Нужно только выделение в куче?"]
    SHARED["Нужно разделяемое владение?"]
    THREADED["Разделяется между потоками?"]
    MUTABLE["Нужна внутренняя изменяемость?"]
    MAYBE_OWN["Иногда заимствуется,<br/>иногда владеется?"]

    BOX["Используйте Box&lt;T&gt;"]
    RC["Используйте Rc&lt;T&gt;"]
    ARC["Используйте Arc&lt;T&gt;"]
    REFCELL["Используйте RefCell&lt;T&gt;<br/>(или Rc&lt;RefCell&lt;T&gt;&gt;)"]
    MUTEX["Используйте Arc&lt;Mutex&lt;T&gt;&gt;"]
    COW["Используйте Cow&lt;'a, T&gt;"]
    OWN["Используйте владеемый тип<br/>(String, Vec и т. д.)"]

    START -->|Да| HEAP
    START -->|Нет| OWN
    HEAP -->|Да| BOX
    HEAP -->|Разделяемое| SHARED
    SHARED -->|Один поток| RC
    SHARED -->|Несколько потоков| THREADED
    THREADED -->|Только чтение| ARC
    THREADED -->|Чтение и запись| MUTEX
    RC -->|Нужна мутация?| MUTABLE
    MUTABLE -->|Да| REFCELL
    MAYBE_OWN -->|Да| COW

    style BOX fill:#e3f2fd,color:#000
    style RC fill:#e8f5e8,color:#000
    style ARC fill:#c8e6c9,color:#000
    style REFCELL fill:#fff3e0,color:#000
    style MUTEX fill:#fff3e0,color:#000
    style COW fill:#e3f2fd,color:#000
    style OWN fill:#f5f5f5,color:#000
🏋️ Упражнение: выберите подходящий умный указатель (нажмите, чтобы раскрыть)

Задача: для каждого сценария выберите подходящий умный указатель и объясните почему.

  1. Рекурсивная древовидная структура данных
  2. Общий объект конфигурации, который читают несколько компонентов (один поток)
  3. Счётчик запросов, разделяемый между обработчиками HTTP-запросов в разных потоках
  4. Кэш, который может возвращать заимствованные или владеемые строки
  5. Буфер логов, который нужно изменять через разделяемую ссылку
🔑 Решение
  1. Box<T> — рекурсивным типам нужна косвенная адресация, чтобы размер был известен на этапе компиляции
  2. Rc<T> — разделяемый доступ только для чтения, один поток, накладные расходы Arc не нужны
  3. Arc<Mutex<u64>> — разделяется между потоками (Arc) с изменением (Mutex)
  4. Cow<'a, str> — иногда возвращает &str (попадание в кэш), иногда String (промах кэша)
  5. RefCell<Vec<String>> — внутренняя изменяемость за &self (один поток)

Эмпирическое правило: начинайте с владеемых типов. Берите Box, когда нужна косвенная адресация, Rc/Arc, когда нужно разделение, RefCell/Mutex, когда нужна внутренняя изменяемость, и Cow, когда хотите работать без копирования в типичном случае.


8. Крейты и модули

Модули и крейты: организация кода

Что вы узнаете: систему модулей Rust в сравнении с пространствами имён и сборками C#, модификаторы видимости pub/pub(crate)/pub(super), организацию модулей по файлам и то, как крейты соответствуют сборкам .NET.

Сложность: 🟢 Начальный

Понимание системы модулей Rust необходимо для организации кода и управления зависимостями. Разработчикам C# это аналогично пониманию пространств имён, сборок и пакетов NuGet.

Модули Rust против пространств имён C#

Организация пространств имён в C#

// Файл: Models/User.cs
namespace MyApp.Models
{
    public class User
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

// Файл: Services/UserService.cs
using MyApp.Models;

namespace MyApp.Services
{
    public class UserService
    {
        public User CreateUser(string name, int age)
        {
            return new User { Name = name, Age = age };
        }
    }
}

// Файл: Program.cs
using MyApp.Models;
using MyApp.Services;

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var service = new UserService();
            var user = service.CreateUser("Alice", 30);
        }
    }
}

Организация модулей в Rust

// Файл: src/models.rs
pub struct User {
    pub name: String,
    pub age: u32,
}

impl User {
    pub fn new(name: String, age: u32) -> User {
        User { name, age }
    }
}

// Файл: src/services.rs
use crate::models::User;

pub struct UserService;

impl UserService {
    pub fn create_user(name: String, age: u32) -> User {
        User::new(name, age)
    }
}

// Файл: src/lib.rs (или main.rs)
pub mod models;
pub mod services;

use models::User;
use services::UserService;

fn main() {
    let service = UserService;
    let user = UserService::create_user("Alice".to_string(), 30);
}

Иерархия модулей и видимость

graph TD
    Crate["crate (корень)"] --> ModA["mod data"]
    Crate --> ModB["mod api"]
    ModA --> SubA1["pub struct Repo"]
    ModA --> SubA2["fn helper  (приватная)"]
    ModB --> SubB1["pub fn handle()"]
    ModB --> SubB2["pub(crate) fn internal()"]
    ModB --> SubB3["pub(super) fn parent_only()"]

    style SubA1 fill:#c8e6c9,color:#000
    style SubA2 fill:#ffcdd2,color:#000
    style SubB1 fill:#c8e6c9,color:#000
    style SubB2 fill:#fff9c4,color:#000
    style SubB3 fill:#fff9c4,color:#000

🟢 Зелёный = публичный везде  |  🟡 Жёлтый = ограниченная видимость  |  🔴 Красный = приватный

Модификаторы видимости в C#

namespace MyApp.Data
{
    // public — доступен откуда угодно
    public class Repository
    {
        // private — только внутри этого класса
        private string connectionString;
        
        // internal — внутри этой сборки
        internal void Connect() { }
        
        // protected — этот класс и его наследники
        protected virtual void Initialize() { }
        
        // public — доступен откуда угодно
        public void Save(object data) { }
    }
}

Правила видимости в Rust

#![allow(unused)]
fn main() {
// В Rust всё по умолчанию приватно
mod data {
    struct Repository {  // Приватная структура
        connection_string: String,  // Приватное поле
    }
    
    impl Repository {
        fn new() -> Repository {  // Приватная функция
            Repository {
                connection_string: "localhost".to_string(),
            }
        }
        
        pub fn connect(&self) {  // Публичный метод
            // Доступен только внутри этого модуля и его потомков
        }
        
        pub(crate) fn initialize(&self) {  // Публичен на уровне крейта
            // Доступен из любого места этого крейта
        }
        
        pub(super) fn internal_method(&self) {  // Публичен для родительского модуля
            // Доступен в родительском модуле
        }
    }
    
    // Публичная структура — доступна извне модуля
    pub struct PublicRepository {
        pub data: String,  // Публичное поле
        private_data: String,  // Приватное поле (без pub)
    }
}

pub use data::PublicRepository;  // Реэкспорт для внешнего использования
}

Организация файлов модулей

Структура проекта C#

MyApp/
├── MyApp.csproj
├── Models/
│   ├── User.cs
│   └── Product.cs
├── Services/
│   ├── UserService.cs
│   └── ProductService.cs
├── Controllers/
│   └── ApiController.cs
└── Program.cs

Структура файлов модулей в Rust

my_app/
├── Cargo.toml
└── src/
    ├── main.rs (или lib.rs)
    ├── models/
    │   ├── mod.rs        // Объявление модуля
    │   ├── user.rs
    │   └── product.rs
    ├── services/
    │   ├── mod.rs        // Объявление модуля
    │   ├── user_service.rs
    │   └── product_service.rs
    └── controllers/
        ├── mod.rs
        └── api_controller.rs

Паттерны объявления модулей

#![allow(unused)]
fn main() {
// src/models/mod.rs
pub mod user;      // Объявляет user.rs как подмодуль
pub mod product;   // Объявляет product.rs как подмодуль

// Реэкспорт часто используемых типов
pub use user::User;
pub use product::Product;

// src/main.rs
mod models;     // Объявляет models/ как модуль
mod services;   // Объявляет services/ как модуль

// Импорт конкретных элементов
use models::{User, Product};
use services::UserService;

// Или импорт всего модуля
use models::user::*;  // Импортирует все публичные элементы модуля user
}

Крейты против сборок .NET

Понятие крейта

В Rust крейт — это базовая единица компиляции и распространения кода, похожая на сборку (assembly) в .NET.

Модель сборок C#

// MyLibrary.dll — скомпилированная сборка
namespace MyLibrary
{
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
    }
}

// MyApp.exe — исполняемая сборка, которая ссылается на MyLibrary.dll
using MyLibrary;

class Program
{
    static void Main()
    {
        var calc = new Calculator();
        Console.WriteLine(calc.Add(2, 3));
    }
}

Модель крейтов Rust

# Cargo.toml для библиотечного крейта
[package]
name = "my_calculator"
version = "0.1.0"
edition = "2021"

[lib]
name = "my_calculator"
#![allow(unused)]
fn main() {
// src/lib.rs — библиотечный крейт
pub struct Calculator;

impl Calculator {
    pub fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
}
}
# Cargo.toml для бинарного крейта, который использует библиотеку
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
my_calculator = { path = "../my_calculator" }
// src/main.rs — бинарный крейт
use my_calculator::Calculator;

fn main() {
    let calc = Calculator;
    println!("{}", calc.add(2, 3));
}

Сравнение типов крейтов

Концепция C#Аналог в RustНазначение
Библиотека классов (.dll)Библиотечный крейтПовторно используемый код
Консольное приложение (.exe)Бинарный крейтИсполняемая программа
Пакет NuGetОпубликованный крейтЕдиница распространения
Сборка (.dll/.exe)Скомпилированный крейтЕдиница компиляции
Solution (.sln)WorkspaceОрганизация нескольких проектов

Workspace против Solution

Структура Solution в C#

<!-- Структура MySolution.sln -->
<Solution>
    <Project Include="WebApi/WebApi.csproj" />
    <Project Include="Business/Business.csproj" />
    <Project Include="DataAccess/DataAccess.csproj" />
    <Project Include="Tests/Tests.csproj" />
</Solution>

Структура Workspace в Rust

# Cargo.toml в корне workspace
[workspace]
members = [
    "web_api",
    "business",
    "data_access",
    "tests"
]

[workspace.dependencies]
serde = "1.0"           # Общие версии зависимостей
tokio = "1.0"
# web_api/Cargo.toml
[package]
name = "web_api"
version = "0.1.0"
edition = "2021"

[dependencies]
business = { path = "../business" }
serde = { workspace = true }    # Использовать версию из workspace
tokio = { workspace = true }

Упражнения

🏋️ Упражнение: спроектируйте дерево модулей (нажмите, чтобы раскрыть)

Дана такая структура проекта на C#. Спроектируйте эквивалентное дерево модулей на Rust:

// C#
namespace MyApp.Services { public class AuthService { } }
namespace MyApp.Services { internal class TokenStore { } }
namespace MyApp.Models { public class User { } }
namespace MyApp.Models { public class Session { } }

Требования:

  1. AuthService и обе модели должны быть публичными
  2. TokenStore должен быть приватным для модуля services
  3. Приведите структуру файлов и объявления mod / pub в lib.rs
🔑 Решение

Структура файлов:

src/
├── lib.rs
├── services/
│   ├── mod.rs
│   ├── auth_service.rs
│   └── token_store.rs
└── models/
    ├── mod.rs
    ├── user.rs
    └── session.rs
// src/lib.rs
pub mod services;
pub mod models;

// src/services/mod.rs
mod token_store;          // приватный — как internal в C#
pub mod auth_service;     // публичный

// src/services/auth_service.rs
use super::token_store::TokenStore; // виден внутри модуля

pub struct AuthService;

impl AuthService {
    pub fn login(&self) { /* использует TokenStore внутри */ }
}

// src/services/token_store.rs
pub(super) struct TokenStore; // виден только родительскому модулю (services)

// src/models/mod.rs
pub mod user;
pub mod session;

// src/models/user.rs
pub struct User {
    pub name: String,
}

// src/models/session.rs
pub struct Session {
    pub user_id: u64,
}

Управление пакетами — Cargo против NuGet

Управление пакетами: Cargo против NuGet

Что вы узнаете: Cargo.toml против .csproj, спецификаторы версий, Cargo.lock, флаги функций для условной компиляции, а также распространённые команды Cargo и их аналоги в NuGet/dotnet.

Сложность: 🟢 Начальный

Объявление зависимостей

Зависимости NuGet в C#

<!-- MyApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  <PackageReference Include="Serilog" Version="3.0.1" />
  <PackageReference Include="Microsoft.AspNetCore.App" />
  
  <ProjectReference Include="../MyLibrary/MyLibrary.csproj" />
</Project>

Зависимости Cargo в Rust

# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde_json = "1.0"               # Из crates.io (как NuGet)
serde = { version = "1.0", features = ["derive"] }  # С функциями
log = "0.4"
tokio = { version = "1.0", features = ["full"] }

# Локальные зависимости (как ProjectReference)
my_library = { path = "../my_library" }

# Зависимости из git
my_git_crate = { git = "https://github.com/user/repo" }

# Зависимости для разработки (как тестовые пакеты)
[dev-dependencies]
criterion = "0.5"               # Бенчмарки
proptest = "1.0"               # Property-тестирование

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

Версионирование пакетов в C#

<!-- Централизованное управление пакетами (Directory.Packages.props) -->
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  
  <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
  <PackageVersion Include="Serilog" Version="3.0.1" />
</Project>

<!-- packages.lock.json для воспроизводимых сборок -->

Управление версиями в Rust

# Cargo.toml — семантическое версионирование
[dependencies]
serde = "1.0"        # Совместимо с 1.x.x (>=1.0.0, <2.0.0)
log = "0.4.17"       # Совместимо с 0.4.x (>=0.4.17, <0.5.0)
regex = "=1.5.4"     # Точная версия
chrono = "^0.4"      # Требования с кареткой (по умолчанию)
uuid = "~1.3.0"      # Требования с тильдой (>=1.3.0, <1.4.0)

# Cargo.lock — точные версии для воспроизводимых сборок (генерируется автоматически)
[[package]]
name = "serde"
version = "1.0.163"
# ... точное дерево зависимостей

Источники пакетов

Источники пакетов в C#

<!-- nuget.config -->
<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="MyCompanyFeed" value="https://pkgs.dev.azure.com/company/_packaging/feed/nuget/v3/index.json" />
  </packageSources>
</configuration>

Источники пакетов в Rust

# .cargo/config.toml
[source.crates-io]
replace-with = "my-awesome-registry"

[source.my-awesome-registry]
registry = "https://my-intranet:8080/index"

# Альтернативные реестры
[registries]
my-registry = { index = "https://my-intranet:8080/index" }

# В Cargo.toml
[dependencies]
my_crate = { version = "1.0", registry = "my-registry" }

Сравнение распространённых команд

ЗадачаКоманда C#Команда Rust
Восстановить пакетыdotnet restorecargo fetch
Добавить пакетdotnet add package Newtonsoft.Jsoncargo add serde_json
Удалить пакетdotnet remove package Newtonsoft.Jsoncargo remove serde_json
Обновить пакетыdotnet updatecargo update
Список пакетовdotnet list packagecargo tree
Аудит безопасностиdotnet list package --vulnerablecargo audit
Чистая сборкаdotnet cleancargo clean

Features: условная компиляция

Условная компиляция в C#

#if DEBUG
    Console.WriteLine("Debug mode");
#elif RELEASE
    Console.WriteLine("Release mode");
#endif

// Функции в файле проекта
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
    <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>

Feature-флаги в Rust

# Cargo.toml
[features]
default = ["json"]              # Функции по умолчанию
json = ["serde_json"]          # Функция, включающая serde_json
xml = ["serde_xml"]            # Альтернативная сериализация
advanced = ["json", "xml"]     # Составная функция

[dependencies]
serde_json = { version = "1.0", optional = true }
serde_xml = { version = "0.4", optional = true }
#![allow(unused)]
fn main() {
// Условная компиляция на основе функций
#[cfg(feature = "json")]
use serde_json;

#[cfg(feature = "xml")]
use serde_xml;

pub fn serialize_data(data: &MyStruct) -> String {
    #[cfg(feature = "json")]
    return serde_json::to_string(data).unwrap();
    
    #[cfg(feature = "xml")]
    return serde_xml::to_string(data).unwrap();
    
    #[cfg(not(any(feature = "json", feature = "xml")))]
    return "No serialization feature enabled".to_string();
}
}

Использование внешних крейтов

Популярные крейты для разработчиков C#

Библиотека C#Крейт RustНазначение
System.Text.Json / Newtonsoft.Jsonserde_jsonСериализация JSON
HttpClientreqwestHTTP-клиент
Entity Frameworkdiesel / sqlxORM / инструментарий SQL
NLog/Seriloglog + env_loggerЛогирование
xUnit/NUnitВстроенный #[test]Модульное тестирование
MoqmockallМоки
FlurlurlРабота с URL
PollytowerПаттерны отказоустойчивости

Пример: миграция HTTP-клиента

// Использование HttpClient в C#
public class ApiClient
{
    private readonly HttpClient _httpClient;
    
    public async Task<User> GetUserAsync(int id)
    {
        var response = await _httpClient.GetAsync($"/users/{id}");
        var json = await response.Content.ReadAsStringAsync();
        return System.Text.Json.JsonSerializer.Deserialize<User>(json);
    }
}
#![allow(unused)]
fn main() {
// Использование reqwest в Rust
use reqwest;
use serde::Deserialize;

#[derive(Deserialize)]
struct User {
    id: u32,
    name: String,
}

struct ApiClient {
    client: reqwest::Client,
}

impl ApiClient {
    async fn get_user(&self, id: u32) -> Result<User, reqwest::Error> {
        let user = self.client
            .get(&format!("https://api.example.com/users/{}", id))
            .send()
            .await?
            .json::<User>()
            .await?;
        
        Ok(user)
    }
}
}

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

Исключения против Result<T, E>

Что вы узнаете: почему Rust заменяет исключения на Result<T, E> и Option<T>, оператор ? для краткого распространения ошибок, и как явная обработка ошибок устраняет скрытый поток управления, который осложняет код с try/catch в C#.

Сложность: 🟡 Средний

См. также: Собственные типы ошибок уровня крейта — производственные паттерны с thiserror и anyhow, и Основные крейты — экосистема крейтов для ошибок.

Обработка ошибок через исключения в C#

// C# — обработка ошибок через исключения
public class UserService
{
    public User GetUser(int userId)
    {
        if (userId <= 0)
        {
            throw new ArgumentException("User ID must be positive");
        }
        
        var user = database.FindUser(userId);
        if (user == null)
        {
            throw new UserNotFoundException($"User {userId} not found");
        }
        
        return user;
    }
    
    public async Task<string> GetUserEmailAsync(int userId)
    {
        try
        {
            var user = GetUser(userId);
            return user.Email ?? throw new InvalidOperationException("User has no email");
        }
        catch (UserNotFoundException ex)
        {
            logger.Warning("User not found: {UserId}", userId);
            return "noreply@company.com";
        }
        catch (Exception ex)
        {
            logger.Error(ex, "Unexpected error getting user email");
            throw; // Повторно выбрасываем
        }
    }
}

Обработка ошибок через Result в Rust

#![allow(unused)]
fn main() {
use std::fmt;

#[derive(Debug)]
pub enum UserError {
    InvalidId(i32),
    NotFound(i32),
    NoEmail,
    DatabaseError(String),
}

impl fmt::Display for UserError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            UserError::InvalidId(id) => write!(f, "Invalid user ID: {}", id),
            UserError::NotFound(id) => write!(f, "User {} not found", id),
            UserError::NoEmail => write!(f, "User has no email address"),
            UserError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
        }
    }
}

impl std::error::Error for UserError {}

#[derive(Debug, Clone)]
pub struct User {
    pub name: String,
    pub email: Option<String>,
}

pub struct UserService {
    users: Vec<User>,  // Имитация базы данных
}

impl UserService {
    fn database_find_user(&self, user_id: i32) -> Option<User> {
        self.users.get(user_id as usize).cloned()
    }

    pub fn get_user(&self, user_id: i32) -> Result<User, UserError> {
        if user_id <= 0 {
            return Err(UserError::InvalidId(user_id));
        }
        
        // Имитация поиска в базе данных
        self.database_find_user(user_id)
            .ok_or(UserError::NotFound(user_id))
    }
    
    pub fn get_user_email(&self, user_id: i32) -> Result<String, UserError> {
        let user = self.get_user(user_id)?; // Оператор ? пробрасывает ошибку
        
        user.email
            .ok_or(UserError::NoEmail)
    }
    
    pub fn get_user_email_or_default(&self, user_id: i32) -> String {
        match self.get_user_email(user_id) {
            Ok(email) => email,
            Err(UserError::NotFound(_)) => {
                log::warn!("User not found: {}", user_id);
                "noreply@company.com".to_string()
            }
            Err(err) => {
                log::error!("Error getting user email: {}", err);
                "error@company.com".to_string()
            }
        }
    }
}
}
graph TD
    subgraph "Модель исключений C#"
        CS_CALL["Вызов метода"]
        CS_SUCCESS["Путь успеха"]
        CS_EXCEPTION["throw Exception"]
        CS_STACK["Раскрутка стека<br/>(затраты во время выполнения)"]
        CS_CATCH["Блок try/catch"]
        CS_HIDDEN["[ERROR] Скрытый поток управления<br/>[ERROR] Затраты на производительность<br/>[ERROR] Легко проигнорировать"]
        
        CS_CALL --> CS_SUCCESS
        CS_CALL --> CS_EXCEPTION
        CS_EXCEPTION --> CS_STACK
        CS_STACK --> CS_CATCH
        CS_EXCEPTION --> CS_HIDDEN
    end
    
    subgraph "Модель Result в Rust"
        RUST_CALL["Вызов функции"]
        RUST_OK["Ok(value)"]
        RUST_ERR["Err(error)"]
        RUST_MATCH["match result"]
        RUST_QUESTION["Оператор ?<br/>(ранний возврат)"]
        RUST_EXPLICIT["[OK] Явная обработка ошибок<br/>[OK] Нулевые затраты во время выполнения<br/>[OK] Ошибки нельзя проигнорировать"]
        
        RUST_CALL --> RUST_OK
        RUST_CALL --> RUST_ERR
        RUST_OK --> RUST_MATCH
        RUST_ERR --> RUST_MATCH
        RUST_ERR --> RUST_QUESTION
        RUST_MATCH --> RUST_EXPLICIT
        RUST_QUESTION --> RUST_EXPLICIT
    end
    
    style CS_HIDDEN fill:#ffcdd2,color:#000
    style RUST_EXPLICIT fill:#c8e6c9,color:#000
    style CS_STACK fill:#fff3e0,color:#000
    style RUST_QUESTION fill:#c8e6c9,color:#000

Оператор ?: краткое распространение ошибок

// C# — распространение исключений (неявное)
public async Task<string> ProcessFileAsync(string path)
{
    var content = await File.ReadAllTextAsync(path);  // Бросает исключение при ошибке
    var processed = ProcessContent(content);          // Бросает исключение при ошибке
    return processed;
}
#![allow(unused)]
fn main() {
// Rust — распространение ошибок через ?
fn process_file(path: &str) -> Result<String, ConfigError> {
    let content = read_config(path)?;  // ? пробрасывает ошибку, если это Err
    let processed = process_content(&content)?;  // ? пробрасывает ошибку, если это Err
    Ok(processed)  // Оборачиваем успешное значение в Ok
}

fn process_content(content: &str) -> Result<String, ConfigError> {
    if content.is_empty() {
        Err(ConfigError::InvalidFormat)
    } else {
        Ok(content.to_uppercase())
    }
}
}

Option<T> для значений, которые могут отсутствовать

// C# — ссылочные типы с поддержкой null
public string? FindUserName(int userId)
{
    var user = database.FindUser(userId);
    return user?.Name;  // Возвращает null, если пользователь не найден
}

public void ProcessUser(int userId)
{
    string? name = FindUserName(userId);
    if (name != null)
    {
        Console.WriteLine($"User: {name}");
    }
    else
    {
        Console.WriteLine("User not found");
    }
}
#![allow(unused)]
fn main() {
// Rust — Option<T> для необязательных значений
fn find_user_name(user_id: u32) -> Option<String> {
    // Имитация поиска в базе данных
    if user_id == 1 {
        Some("Alice".to_string())
    } else {
        None
    }
}

fn process_user(user_id: u32) {
    match find_user_name(user_id) {
        Some(name) => println!("User: {}", name),
        None => println!("User not found"),
    }
    
    // Или используйте if let (сокращённая форма сопоставления с образцом)
    if let Some(name) = find_user_name(user_id) {
        println!("User: {}", name);
    } else {
        println!("User not found");
    }
}
}

Комбинирование Option и Result

fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b != 0.0 {
        Some(a / b)
    } else {
        None
    }
}

fn parse_and_divide(a_str: &str, b_str: &str) -> Result<Option<f64>, ParseFloatError> {
    let a: f64 = a_str.parse()?;  // Возвращаем ошибку разбора, если значение некорректно
    let b: f64 = b_str.parse()?;  // Возвращаем ошибку разбора, если значение некорректно
    Ok(safe_divide(a, b))         // Возвращаем Ok(Some(result)) или Ok(None)
}

use std::num::ParseFloatError;

fn main() {
    match parse_and_divide("10.0", "2.0") {
        Ok(Some(result)) => println!("Result: {}", result),
        Ok(None) => println!("Division by zero"),
        Err(error) => println!("Parse error: {}", error),
    }
}

🏋️ Упражнение: создайте тип ошибок уровня крейта (нажмите, чтобы раскрыть)

Задача: создайте перечисление AppError для приложения обработки файлов, которое может завершиться ошибкой из-за ошибок ввода-вывода, ошибок разбора JSON и ошибок валидации. Реализуйте преобразования From для автоматического распространения через ?.

#![allow(unused)]
fn main() {
// Стартовый код
use std::io;

// TODO: Определите AppError с вариантами:
//   Io(io::Error), Json(serde_json::Error), Validation(String)
// TODO: Реализуйте трейты Display и Error
// TODO: Реализуйте From<io::Error> и From<serde_json::Error>
// TODO: Определите псевдоним типа: type Result<T> = std::result::Result<T, AppError>;

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)?;  // io::Error → AppError
    let config: Config = serde_json::from_str(&content)?;  // ошибка serde → AppError
    if config.name.is_empty() {
        return Err(AppError::Validation("name cannot be empty".into()));
    }
    Ok(config)
}
}
🔑 Решение
#![allow(unused)]
fn main() {
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("I/O error: {0}")]
    Io(#[from] io::Error),

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Validation: {0}")]
    Validation(String),
}

pub type Result<T> = std::result::Result<T, AppError>;

#[derive(serde::Deserialize)]
struct Config {
    name: String,
    port: u16,
}

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)?;
    let config: Config = serde_json::from_str(&content)?;
    if config.name.is_empty() {
        return Err(AppError::Validation("name cannot be empty".into()));
    }
    Ok(config)
}
}

Ключевые выводы:

  • thiserror генерирует реализации Display и Error на основе атрибутов
  • #[from] генерирует реализации From<T>, что позволяет автоматически преобразовывать ошибки через ?
  • Псевдоним Result<T> убирает шаблонный код во всём крейте
  • В отличие от исключений C#, тип ошибки виден в каждой сигнатуре функции

Типы ошибок уровня крейта и псевдонимы Result

Типы ошибок уровня крейта и псевдонимы Result

Что вы узнаете: производственный паттерн определения перечисления ошибок для каждого крейта с помощью thiserror, создание псевдонима типа Result<T>, а также когда выбирать thiserror (библиотеки) или anyhow (приложения).

Сложность: 🟡 Средний

Ключевой паттерн для производственного кода на Rust: определить перечисление ошибок для каждого крейта и псевдоним типа Result, чтобы убрать шаблонный код.

Паттерн

#![allow(unused)]
fn main() {
// src/error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("Validation error: {message}")]
    Validation { message: String },

    #[error("Not found: {entity} with id {id}")]
    NotFound { entity: String, id: String },
}

/// Псевдоним Result для всего крейта — каждая функция возвращает именно его
pub type Result<T> = std::result::Result<T, AppError>;
}

Использование во всём крейте

#![allow(unused)]
fn main() {
use crate::error::{AppError, Result};

// Предполагается, что пул соединений с базой доступен, например:
// async fn get_user(pool: &PgPool, id: Uuid) -> Result<User>
// Здесь показан паттерн, где `pool` — сокращение.
pub async fn get_user(id: Uuid) -> Result<User> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_optional(&pool)
        .await?;  // sqlx::Error → AppError::Database через #[from]

    user.ok_or_else(|| AppError::NotFound {
        entity: "User".into(),
        id: id.to_string(),
    })
}

pub async fn create_user(req: CreateUserRequest) -> Result<User> {
    if req.name.trim().is_empty() {
        return Err(AppError::Validation {
            message: "Name cannot be empty".into(),
        });
    }
    // ...
}
}

Сравнение с C#

// Аналогичный паттерн в C#
public class AppException : Exception
{
    public string ErrorCode { get; }
    public AppException(string code, string message) : base(message)
    {
        ErrorCode = code;
    }
}

// Но в C# вызывающий код не знает, какие исключения ожидать!
// В Rust тип ошибки указан в сигнатуре функции.

Почему это важно

  • thiserror автоматически генерирует реализации Display и Error
  • #[from] позволяет оператору ? автоматически преобразовывать ошибки библиотек
  • Псевдоним Result<T> делает каждую сигнатуру функции чистой: fn foo() -> Result<Bar>
  • В отличие от исключений C# вызывающий код видит все возможные варианты ошибок в типе

thiserror против anyhow: что выбрать

Обработку ошибок в Rust определяют два крейта. Выбор между ними — первое решение, которое вам предстоит принять:

thiserroranyhow
НазначениеОпределение структурированных типов ошибок для библиотекБыстрая обработка ошибок для приложений
РезультатСобственное перечисление, которым вы управляетеНепрозрачная обёртка anyhow::Error
Что видит вызывающий кодВсе варианты ошибок в типеПросто anyhow::Error — непрозрачный
Лучше всего дляБиблиотечных крейтов, API, любого кода с потребителямиБинарников, скриптов, прототипов, CLI-утилит
Приведение типовmatch по вариантам напрямуюerror.downcast_ref::<MyError>()
#![allow(unused)]
fn main() {
// thiserror — для БИБЛИОТЕК (вызывающему коду нужно сопоставлять варианты ошибок)
use thiserror::Error;

#[derive(Error, Debug)]
pub enum StorageError {
    #[error("File not found: {path}")]
    NotFound { path: String },

    #[error("Permission denied: {0}")]
    PermissionDenied(String),

    #[error(transparent)]
    Io(#[from] std::io::Error),
}

pub fn read_config(path: &str) -> Result<String, StorageError> {
    std::fs::read_to_string(path).map_err(|e| match e.kind() {
        std::io::ErrorKind::NotFound => StorageError::NotFound { path: path.into() },
        std::io::ErrorKind::PermissionDenied => StorageError::PermissionDenied(path.into()),
        _ => StorageError::Io(e),
    })
}
}
// anyhow — для ПРИЛОЖЕНИЙ (просто пробрасываем ошибки, не определяя типов)
use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;

    let port: u16 = config.parse()
        .context("Failed to parse port number")?;

    println!("Listening on port {port}");
    Ok(())
}
// anyhow::Result<T> = Result<T, anyhow::Error>
// .context() добавляет понятный для человека контекст к любой ошибке
// Сравнение с C#:
// thiserror ≈ определение собственных классов исключений с конкретными свойствами
// anyhow ≈ перехват Exception и оборачивание с сообщением:
//   throw new InvalidOperationException("Failed to read config", ex);

Правило: если ваш код — библиотека (его вызывает другой код), используйте thiserror. Если ваш код — приложение (финальный бинарник), используйте anyhow. Многие проекты используют оба: thiserror для публичного API библиотечного крейта, anyhow в бинарнике в main().

Паттерны восстановления после ошибок

Разработчики C# привыкли к блокам try/catch, которые восстанавливаются после конкретных исключений. В Rust для того же используются комбинаторы над Result:

#![allow(unused)]
fn main() {
use std::fs;

// Паттерн 1: восстановление через значение по умолчанию
let config = fs::read_to_string("config.toml")
    .unwrap_or_else(|_| String::from("port = 8080"));  // значение по умолчанию, если файла нет

// Паттерн 2: восстанавливаемся после конкретных ошибок, остальные пробрасываем
fn read_or_create(path: &str) -> Result<String, std::io::Error> {
    match fs::read_to_string(path) {
        Ok(content) => Ok(content),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            let default = String::from("# new file");
            fs::write(path, &default)?;
            Ok(default)
        }
        Err(e) => Err(e),  // пробрасываем ошибки доступа и прочие
    }
}

// Паттерн 3: добавляем контекст перед распространением
use anyhow::Context;

fn load_config() -> anyhow::Result<Config> {
    let text = fs::read_to_string("config.toml")
        .context("Failed to read config.toml")?;
    let config: Config = toml::from_str(&text)
        .context("Failed to parse config.toml")?;
    Ok(config)
}

// Паттерн 4: преобразуем ошибки в доменный тип
fn parse_port(s: &str) -> Result<u16, AppError> {
    s.parse::<u16>()
        .map_err(|_| AppError::Validation {
            message: format!("Invalid port: {s}"),
        })
}
}
// Аналоги в C#:
try { config = File.ReadAllText("config.toml"); }
catch (FileNotFoundException) { config = "port = 8080"; }  // Паттерн 1

try { /* ... */ }
catch (FileNotFoundException) { /* создать файл */ }        // Паттерн 2
catch { throw; }                                            // остальные пробрасываем

Когда восстанавливаться, а когда пробрасывать:

  • Восстанавливайтесь, когда у ошибки есть разумное значение по умолчанию или стратегия повтора
  • Пробрасывайте через ?, когда решение должен принимать вызывающий код
  • Добавляйте контекст (.context()) на границах модулей, чтобы построить цепочку ошибок

Упражнения

🏋️ Упражнение: спроектируйте тип ошибок крейта (нажмите, чтобы раскрыть)

Вы создаёте сервис регистрации пользователей. Спроектируйте тип ошибок с помощью thiserror:

  1. Определите RegistrationError с вариантами: DuplicateEmail(String), WeakPassword(String), DatabaseError(#[from] sqlx::Error), RateLimited { retry_after_secs: u64 }
  2. Создайте псевдоним type Result<T> = std::result::Result<T, RegistrationError>;
  3. Напишите register_user(email: &str, password: &str) -> Result<()>, которая демонстрирует распространение через ? и явное создание ошибок
🔑 Решение
#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Error, Debug)]
pub enum RegistrationError {
    #[error("Email already registered: {0}")]
    DuplicateEmail(String),

    #[error("Password too weak: {0}")]
    WeakPassword(String),

    #[error("Database error")]
    Database(#[from] sqlx::Error),

    #[error("Rate limited — retry after {retry_after_secs}s")]
    RateLimited { retry_after_secs: u64 },
}

pub type Result<T> = std::result::Result<T, RegistrationError>;

pub fn register_user(email: &str, password: &str) -> Result<()> {
    if password.len() < 8 {
        return Err(RegistrationError::WeakPassword(
            "must be at least 8 characters".into(),
        ));
    }

    // Этот ? преобразует sqlx::Error → RegistrationError::Database автоматически
    // db.check_email_unique(email).await?;

    // Это явное создание ошибки для доменной логики
    if email.contains("+spam") {
        return Err(RegistrationError::DuplicateEmail(email.to_string()));
    }

    Ok(())
}
}

Ключевой паттерн: #[from] включает ? для ошибок библиотек; явный Err(...) — для доменной логики. Псевдоним Result делает каждую сигнатуру чистой.


10. Трейты и обобщения

Трейты — интерфейсы Rust

Что вы узнаете: трейты против интерфейсов C#, реализации методов по умолчанию, трейт-объекты (dyn Trait) против ограничений обобщений (impl Trait), производные трейты, распространённые трейты стандартной библиотеки, ассоциированные типы и перегрузку операторов через трейты.

Сложность: 🟡 Средний

Трейты — способ Rust описывать общее поведение. Они похожи на интерфейсы C#, но мощнее.

Сравнение с интерфейсами C#

// Определение интерфейса в C#
public interface IAnimal
{
    string Name { get; }
    void MakeSound();
    
    // Реализация по умолчанию (C# 8+)
    string Describe()
    {
        return $"{Name} makes a sound";
    }
}

// Реализация интерфейса в C#
public class Dog : IAnimal
{
    public string Name { get; }
    
    public Dog(string name)
    {
        Name = name;
    }
    
    public void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
    
    // Можно переопределить реализацию по умолчанию
    public string Describe()
    {
        return $"{Name} is a loyal dog";
    }
}

// Ограничения обобщений
public void ProcessAnimal<T>(T animal) where T : IAnimal
{
    animal.MakeSound();
    Console.WriteLine(animal.Describe());
}

Определение и реализация трейтов в Rust

// Определение трейта
trait Animal {
    fn name(&self) -> &str;
    fn make_sound(&self);
    
    // Реализация по умолчанию
    fn describe(&self) -> String {
        format!("{} makes a sound", self.name())
    }
    
    // Реализация по умолчанию, использующая другие методы трейта
    fn introduce(&self) {
        println!("Hi, I'm {}", self.name());
        self.make_sound();
    }
}

// Определение структуры
#[derive(Debug)]
struct Dog {
    name: String,
    breed: String,
}

impl Dog {
    fn new(name: String, breed: String) -> Dog {
        Dog { name, breed }
    }
}

// Реализация трейта
impl Animal for Dog {
    fn name(&self) -> &str {
        &self.name
    }
    
    fn make_sound(&self) {
        println!("Woof!");
    }
    
    // Переопределяем реализацию по умолчанию
    fn describe(&self) -> String {
        format!("{} is a loyal {} dog", self.name, self.breed)
    }
}

// Ещё одна реализация
#[derive(Debug)]
struct Cat {
    name: String,
    indoor: bool,
}

impl Animal for Cat {
    fn name(&self) -> &str {
        &self.name
    }
    
    fn make_sound(&self) {
        println!("Meow!");
    }
    
    // Используем describe() по умолчанию
}

// Обобщённая функция с ограничениями трейтов
fn process_animal<T: Animal>(animal: &T) {
    animal.make_sound();
    println!("{}", animal.describe());
    animal.introduce();
}

// Несколько ограничений трейтов
fn process_animal_debug<T: Animal + std::fmt::Debug>(animal: &T) {
    println!("Debug: {:?}", animal);
    process_animal(animal);
}

fn main() {
    let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string());
    let cat = Cat { name: "Whiskers".to_string(), indoor: true };
    
    process_animal(&dog);
    process_animal(&cat);
    
    process_animal_debug(&dog);
}

Трейт-объекты и динамическая диспетчеризация

// Динамический полиморфизм в C#
public void ProcessAnimals(List<IAnimal> animals)
{
    foreach (var animal in animals)
    {
        animal.MakeSound(); // Динамическая диспетчеризация
        Console.WriteLine(animal.Describe());
    }
}

// Использование
var animals = new List<IAnimal>
{
    new Dog("Buddy"),
    new Cat("Whiskers"),
    new Dog("Rex")
};

ProcessAnimals(animals);
// Трейт-объекты Rust для динамической диспетчеризации
fn process_animals(animals: &[Box<dyn Animal>]) {
    for animal in animals {
        animal.make_sound(); // Динамическая диспетчеризация
        println!("{}", animal.describe());
    }
}

// Альтернатива: использование ссылок
fn process_animal_refs(animals: &[&dyn Animal]) {
    for animal in animals {
        animal.make_sound();
        println!("{}", animal.describe());
    }
}

fn main() {
    // Использование Box<dyn Trait>
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog::new("Buddy".to_string(), "Golden Retriever".to_string())),
        Box::new(Cat { name: "Whiskers".to_string(), indoor: true }),
        Box::new(Dog::new("Rex".to_string(), "German Shepherd".to_string())),
    ];
    
    process_animals(&animals);
    
    // Использование ссылок
    let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string());
    let cat = Cat { name: "Whiskers".to_string(), indoor: true };
    
    let animal_refs: Vec<&dyn Animal> = vec![&dog, &cat];
    process_animal_refs(&animal_refs);
}

Производные трейты

// Автоматически выводим распространённые трейты
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Person {
    name: String,
    age: u32,
}

// Что генерируется (упрощённо):
impl std::fmt::Debug for Person {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Person")
            .field("name", &self.name)
            .field("age", &self.age)
            .finish()
    }
}

impl Clone for Person {
    fn clone(&self) -> Self {
        Person {
            name: self.name.clone(),
            age: self.age,
        }
    }
}

impl PartialEq for Person {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.age == other.age
    }
}

// Использование
fn main() {
    let person1 = Person {
        name: "Alice".to_string(),
        age: 30,
    };
    
    let person2 = person1.clone(); // Трейт Clone
    
    println!("{:?}", person1); // Трейт Debug
    println!("Equal: {}", person1 == person2); // Трейт PartialEq
}

Распространённые трейты стандартной библиотеки

use std::collections::HashMap;

// Трейт Display для удобного для пользователя вывода
impl std::fmt::Display for Person {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} (age {})", self.name, self.age)
    }
}

// Трейт From для преобразований
impl From<(String, u32)> for Person {
    fn from((name, age): (String, u32)) -> Self {
        Person { name, age }
    }
}

// Трейт Into реализуется автоматически, когда реализован From
fn create_person() {
    let person: Person = ("Alice".to_string(), 30).into();
    println!("{}", person);
}

// Реализация трейта Iterator
struct PersonIterator {
    people: Vec<Person>,
    index: usize,
}

impl Iterator for PersonIterator {
    type Item = Person;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.index < self.people.len() {
            let person = self.people[self.index].clone();
            self.index += 1;
            Some(person)
        } else {
            None
        }
    }
}

impl Person {
    fn iterator(people: Vec<Person>) -> PersonIterator {
        PersonIterator { people, index: 0 }
    }
}

fn main() {
    let people = vec![
        Person::from(("Alice".to_string(), 30)),
        Person::from(("Bob".to_string(), 25)),
        Person::from(("Charlie".to_string(), 35)),
    ];
    
    // Используем наш собственный итератор
    for person in Person::iterator(people.clone()) {
        println!("{}", person); // Использует трейт Display
    }
}

🏋️ Упражнение: система рисования на основе трейтов (нажмите, чтобы раскрыть)

Задача: реализуйте трейт Drawable с методом area() и методом draw() по умолчанию. Создайте структуры Circle и Rect. Напишите функцию, которая принимает &[Box<dyn Drawable>] и выводит суммарную площадь.

🔑 Решение
use std::f64::consts::PI;

trait Drawable {
    fn area(&self) -> f64;

    fn draw(&self) {
        println!("Drawing shape with area {:.2}", self.area());
    }
}

struct Circle { radius: f64 }
struct Rect   { w: f64, h: f64 }

impl Drawable for Circle {
    fn area(&self) -> f64 { PI * self.radius * self.radius }
}

impl Drawable for Rect {
    fn area(&self) -> f64 { self.w * self.h }
}

fn total_area(shapes: &[Box<dyn Drawable>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn main() {
    let shapes: Vec<Box<dyn Drawable>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Rect { w: 4.0, h: 6.0 }),
        Box::new(Circle { radius: 2.0 }),
    ];
    for s in &shapes { s.draw(); }
    println!("Total area: {:.2}", total_area(&shapes));
}

Ключевые выводы:

  • dyn Trait даёт полиморфизм времени выполнения (как IDrawable в C#)
  • Box<dyn Trait> размещается в куче и нужен для коллекций разнородных типов
  • Методы по умолчанию работают точно так же, как методы интерфейсов по умолчанию в C# 8+

Ассоциированные типы: трейты с типовыми членами

Интерфейсы C# не имеют ассоциированных типов — трейты Rust имеют. Так устроен Iterator:

#![allow(unused)]
fn main() {
// Трейт Iterator имеет ассоциированный тип 'Item'
trait Iterator {
    type Item;                         // Каждая реализация определяет, что такое Item
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter { max: u32, current: u32 }

impl Iterator for Counter {
    type Item = u32;                   // Этот Counter выдаёт значения u32
    fn next(&mut self) -> Option<u32> {
        if self.current < self.max {
            self.current += 1;
            Some(self.current)
        } else {
            None
        }
    }
}
}

В C# для этой цели IEnumerator<T> использует параметр обобщения (T). Ассоциированные типы Rust устроены иначе: у Iterator один тип Item на каждую реализацию, а не параметр обобщения на уровне трейта. Это упрощает ограничения трейтов: impl Iterator<Item = u32> против IEnumerable<int> в C#.

Перегрузка операторов через трейты

В C# вы определяете public static MyType operator+(MyType a, MyType b). В Rust каждый оператор соответствует трейту из std::ops:

#![allow(unused)]
fn main() {
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }

impl Add for Vec2 {
    type Output = Vec2;
    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let c = a + b;  // вызывает <Vec2 as Add>::add(a, b)
}
C#RustПримечания
operator+impl Addself передаётся по значению — для типов, не реализующих Copy, это потребление
operator==impl PartialEqОбычно #[derive(PartialEq)]
operator<impl PartialOrdОбычно #[derive(PartialOrd)]
ToString()impl fmt::DisplayИспользуется в println!("{}", x)
Неявное преобразованиеАналога нетВ Rust нет неявных преобразований — используйте From/Into

Согласованность: правило сироты

Трейт можно реализовать, только если вы владеете либо трейтом, либо типом. Это предотвращает конфликтующие реализации между крейтами:

#![allow(unused)]
fn main() {
// ✅ OK — вы владеете MyType
impl Display for MyType { ... }

// ✅ OK — вы владеете MyTrait
impl MyTrait for String { ... }

// ❌ ОШИБКА — вы не владеете ни Display, ни String
impl Display for String { ... }
}

В C# такого ограничения нет — любой код может добавить методы расширения к любому типу, что может привести к неоднозначности.

impl Trait: возврат трейтов без упаковки в Box

Интерфейсы C# всегда можно использовать как возвращаемые типы. В Rust возврат трейта требует выбора: статическая диспетчеризация (impl Trait) или динамическая (dyn Trait).

impl Trait в позиции аргумента (сокращённая запись обобщения)

#![allow(unused)]
fn main() {
// Эти две записи эквивалентны:
fn print_animal(animal: &impl Animal) { animal.make_sound(); }
fn print_animal<T: Animal>(animal: &T)  { animal.make_sound(); }

// impl Trait — это просто синтаксический сахар для параметра обобщения
// Компилятор генерирует специализированную копию для каждого конкретного типа (мономорфизация)
}

impl Trait в позиции возвращаемого значения (ключевое отличие)

// Возвращаем итератор, не раскрывая конкретный тип
fn even_squares(limit: u32) -> impl Iterator<Item = u32> {
    (0..limit)
        .filter(|n| n % 2 == 0)
        .map(|n| n * n)
}
// Вызывающий код видит «какой-то тип, реализующий Iterator<Item = u32>»
// Фактический тип (Filter<Map<Range<u32>, ...>>) не имеет имени — impl Trait решает эту проблему

fn main() {
    for n in even_squares(20) {
        print!("{n} ");
    }
    // Вывод: 0 4 16 36 64 100 144 196 256 324
}
// C# — возврат интерфейса (всегда динамическая диспетчеризация, объект итератора в куче)
public IEnumerable<int> EvenSquares(int limit) =>
    Enumerable.Range(0, limit)
        .Where(n => n % 2 == 0)
        .Select(n => n * n);
// Тип возвращаемого значения скрывает конкретный итератор за интерфейсом IEnumerable
// В отличие от Box<dyn Trait> в Rust, C# не упаковывает явно — выделением памяти занимается рантайм

Возврат замыканий: impl Fn против Box<dyn Fn>

#![allow(unused)]
fn main() {
// Возвращаем замыкание — тип замыкания назвать нельзя, поэтому impl Fn необходим
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

let add5 = make_adder(5);
println!("{}", add5(3)); // 8

// Если нужно условно возвращать РАЗНЫЕ замыкания, понадобится Box:
fn choose_op(add: bool) -> Box<dyn Fn(i32, i32) -> i32> {
    if add {
        Box::new(|a, b| a + b)
    } else {
        Box::new(|a, b| a * b)
    }
}
// impl Trait требует ОДНОГО конкретного типа; разные замыкания — это разные типы
}
// C# — делегаты решают это естественно (всегда выделяются в куче)
Func<int, int> MakeAdder(int x) => y => x + y;
Func<int, int, int> ChooseOp(bool add) => add ? (a, b) => a + b : (a, b) => a * b;

Решение о диспетчеризации: impl Trait против dyn Trait против обобщений

С этим архитектурным решением разработчики C# сталкиваются в Rust сразу. Вот полное руководство:

graph TD
    START["Функция принимает или возвращает<br/>тип на основе трейта?"]
    POSITION["Позиция аргумента или возврата?"]
    ARG_SAME["Все вызывающие передают<br/>один и тот же тип?"]
    RET_SINGLE["Всегда возвращается один и тот же<br/>конкретный тип?"]
    COLLECTION["Хранится в коллекции<br/>или как поле структуры?"]

    GENERIC["Используйте обобщения<br/><code>fn foo&lt;T: Trait&gt;(x: T)</code>"]
    IMPL_ARG["Используйте impl Trait<br/><code>fn foo(x: impl Trait)</code>"]
    IMPL_RET["Используйте impl Trait<br/><code>fn foo() -> impl Trait</code>"]
    DYN_BOX["Используйте Box&lt;dyn Trait&gt;<br/>Динамическая диспетчеризация"]
    DYN_REF["Используйте &dyn Trait<br/>Динамическая диспетчеризация по ссылке"]

    START --> POSITION
    POSITION -->|Аргумент| ARG_SAME
    POSITION -->|Возврат| RET_SINGLE
    ARG_SAME -->|"Да (синтаксический сахар)"| IMPL_ARG
    ARG_SAME -->|"Сложные ограничения/несколько использований"| GENERIC
    RET_SINGLE -->|Да| IMPL_RET
    RET_SINGLE -->|"Нет (условные типы)"| DYN_BOX
    RET_SINGLE -->|"Коллекция разнородных типов"| COLLECTION
    COLLECTION -->|Владеемая| DYN_BOX
    COLLECTION -->|Заимствованная| DYN_REF

    style GENERIC fill:#c8e6c9,color:#000
    style IMPL_ARG fill:#c8e6c9,color:#000
    style IMPL_RET fill:#c8e6c9,color:#000
    style DYN_BOX fill:#fff3e0,color:#000
    style DYN_REF fill:#fff3e0,color:#000
ПодходДиспетчеризацияВыделение памятиКогда использовать
fn foo<T: Trait>(x: T)Статическая (мономорфизация)СтекНесколько ограничений трейтов, нужен turbofish, один и тот же тип используется повторно
fn foo(x: impl Trait)Статическая (мономорфизация)СтекПростые ограничения, более чистый синтаксис, разовые параметры
fn foo() -> impl TraitСтатическаяСтекОдин конкретный тип возвращаемого значения, итераторы, замыкания
fn foo() -> Box<dyn Trait>Динамическая (таблица виртуальных методов)КучаРазные типы возвращаемых значений, трейт-объекты в коллекциях
&dyn Trait / &mut dyn TraitДинамическая (таблица виртуальных методов)Без выделенияЗаимствованные ссылки на разнородные типы, параметры функций
#![allow(unused)]
fn main() {
// Сводка: от самого быстрого к самому гибкому
fn static_dispatch(x: impl Display)             { /* быстрее всего, без выделения */ }
fn generic_dispatch<T: Display + Clone>(x: T)    { /* быстрее всего, несколько ограничений */ }
fn dynamic_dispatch(x: &dyn Display)             { /* поиск в таблице виртуальных методов, без выделения */ }
fn boxed_dispatch(x: Box<dyn Display>)           { /* поиск в таблице виртуальных методов + выделение в куче */ }
}

Ограничения обобщений

Ограничения обобщений: where и ограничения трейтов

Что вы узнаете: ограничения трейтов Rust в сравнении с ограничениями where в C#, синтаксис предложения where, условные реализации трейтов, ассоциированные типы и границы трейтов высокого ранга (HRTB).

Сложность: 🔴 Продвинутый

Ограничения обобщений в C#

// Ограничения обобщений в C# через предложение where
public class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        return new T();  // Ограничение new() позволяет конструктор без параметров
    }
    
    public void Save(T entity)
    {
        if (entity.Id == 0)  // Ограничение IEntity даёт свойство Id
        {
            entity.Id = GenerateId();
        }
        // Сохранение в базу данных
    }
}

// Несколько параметров типа с ограничениями
public class Converter<TInput, TOutput> 
    where TInput : IConvertible
    where TOutput : class, new()
{
    public TOutput Convert(TInput input)
    {
        var output = new TOutput();
        // Логика преобразования через IConvertible
        return output;
    }
}

// Вариантность в обобщениях
public interface IRepository<out T> where T : IEntity
{
    IEnumerable<T> GetAll();  // Ковариантность — может возвращать более производные типы
}

public interface IWriter<in T> where T : IEntity
{
    void Write(T entity);  // Контравариантность — может принимать более базовые типы
}

Ограничения обобщений Rust через трейт-границы

#![allow(unused)]
fn main() {
use std::fmt::{Debug, Display};
use std::clone::Clone;

// Базовые трейт-границы
pub struct Repository<T> 
where 
    T: Clone + Debug + Default,
{
    items: Vec<T>,
}

impl<T> Repository<T> 
where 
    T: Clone + Debug + Default,
{
    pub fn new() -> Self {
        Repository { items: Vec::new() }
    }
    
    pub fn create(&self) -> T {
        T::default()  // Трейт Default предоставляет значение по умолчанию
    }
    
    pub fn add(&mut self, item: T) {
        println!("Adding item: {:?}", item);  // Трейт Debug для вывода
        self.items.push(item);
    }
    
    pub fn get_all(&self) -> Vec<T> {
        self.items.clone()  // Трейт Clone для копирования
    }
}

// Несколько трейт-границ с разным синтаксисом
pub fn process_data<T, U>(input: T) -> U 
where 
    T: Display + Clone,
    U: From<T> + Debug,
{
    println!("Processing: {}", input);  // Трейт Display
    let cloned = input.clone();         // Трейт Clone
    let output = U::from(cloned);       // Трейт From для преобразования
    println!("Result: {:?}", output);   // Трейт Debug
    output
}

// Ассоциированные типы (аналог ограничений обобщений в C#)
pub trait Iterator {
    type Item;  // Ассоциированный тип вместо параметра обобщения
    
    fn next(&mut self) -> Option<Self::Item>;
}

pub trait Collect<T> {
    fn collect<I: Iterator<Item = T>>(iter: I) -> Self;
}

// Границы трейтов высокого ранга (продвинутый уровень)
fn apply_to_all<F>(items: &[String], f: F) -> Vec<String>
where 
    F: for<'a> Fn(&'a str) -> String,  // Функция работает с любым временем жизни
{
    items.iter().map(|s| f(s)).collect()
}

// Условные реализации трейтов
impl<T> PartialEq for Repository<T> 
where 
    T: PartialEq + Clone + Debug + Default,
{
    fn eq(&self, other: &Self) -> bool {
        self.items == other.items
    }
}
}
graph TD
    subgraph "Ограничения обобщений в C#"
        CS_WHERE["where T : class, IInterface, new()"]
        CS_RUNTIME["[ERROR] Проверка типов частично во время выполнения<br/>Диспетчеризация виртуальных методов"]
        CS_VARIANCE["[OK] Ковариантность/контравариантность<br/>ключевые слова in/out"]
        CS_REFLECTION["[ERROR] Возможна рефлексия во время выполнения<br/>операторы typeof(T), is, as"]
        CS_BOXING["[ERROR] Упаковка значимых типов<br/>для ограничений интерфейсов"]
        
        CS_WHERE --> CS_RUNTIME
        CS_WHERE --> CS_VARIANCE
        CS_WHERE --> CS_REFLECTION
        CS_WHERE --> CS_BOXING
    end
    
    subgraph "Трейт-границы Rust"
        RUST_WHERE["where T: Trait + Clone + Debug"]
        RUST_COMPILE["[OK] Разрешение на этапе компиляции<br/>Мономорфизация"]
        RUST_ZERO["[OK] Абстракции с нулевой стоимостью<br/>Без накладных расходов рантайма"]
        RUST_ASSOCIATED["[OK] Ассоциированные типы<br/>Гибче, чем обобщения"]
        RUST_HKT["[OK] Границы трейтов высокого ранга<br/>Продвинутые связи между типами"]
        
        RUST_WHERE --> RUST_COMPILE
        RUST_WHERE --> RUST_ZERO
        RUST_WHERE --> RUST_ASSOCIATED
        RUST_WHERE --> RUST_HKT
    end
    
    subgraph "Сравнение гибкости"
        CS_FLEX["Гибкость C#<br/>[OK] Вариантность<br/>[OK] Информация о типах во время выполнения<br/>[ERROR] Затраты на производительность"]
        RUST_FLEX["Гибкость Rust<br/>[OK] Нулевая стоимость<br/>[OK] Безопасность на этапе компиляции<br/>[ERROR] Вариантности пока нет"]
    end
    
    style CS_RUNTIME fill:#fff3e0,color:#000
    style CS_BOXING fill:#ffcdd2,color:#000
    style RUST_COMPILE fill:#c8e6c9,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000
    style CS_FLEX fill:#e3f2fd,color:#000
    style RUST_FLEX fill:#c8e6c9,color:#000

Упражнения

🏋️ Упражнение: обобщённый репозиторий (нажмите, чтобы раскрыть)

Переведите этот обобщённый интерфейс репозитория C# на трейты Rust:

public interface IRepository<T> where T : IEntity, new()
{
    T GetById(int id);
    IEnumerable<T> Find(Func<T, bool> predicate);
    void Save(T entity);
}

Требования:

  1. Определите трейт Entity с методом fn id(&self) -> u64
  2. Определите трейт Repository<T>, где T: Entity + Clone
  3. Реализуйте InMemoryRepository<T>, который хранит элементы в Vec<T>
  4. Метод find должен принимать impl Fn(&T) -> bool
🔑 Решение
trait Entity: Clone {
    fn id(&self) -> u64;
}

trait Repository<T: Entity> {
    fn get_by_id(&self, id: u64) -> Option<&T>;
    fn find(&self, predicate: impl Fn(&T) -> bool) -> Vec<&T>;
    fn save(&mut self, entity: T);
}

struct InMemoryRepository<T> {
    items: Vec<T>,
}

impl<T: Entity> InMemoryRepository<T> {
    fn new() -> Self { Self { items: Vec::new() } }
}

impl<T: Entity> Repository<T> for InMemoryRepository<T> {
    fn get_by_id(&self, id: u64) -> Option<&T> {
        self.items.iter().find(|item| item.id() == id)
    }
    fn find(&self, predicate: impl Fn(&T) -> bool) -> Vec<&T> {
        self.items.iter().filter(|item| predicate(item)).collect()
    }
    fn save(&mut self, entity: T) {
        if let Some(pos) = self.items.iter().position(|e| e.id() == entity.id()) {
            self.items[pos] = entity;
        } else {
            self.items.push(entity);
        }
    }
}

#[derive(Clone, Debug)]
struct User { user_id: u64, name: String }

impl Entity for User {
    fn id(&self) -> u64 { self.user_id }
}

fn main() {
    let mut repo = InMemoryRepository::new();
    repo.save(User { user_id: 1, name: "Alice".into() });
    repo.save(User { user_id: 2, name: "Bob".into() });

    let found = repo.find(|u| u.name.starts_with('A'));
    assert_eq!(found.len(), 1);
}

Ключевые отличия от C#: ограничения new() нет (вместо него используется трейт Default). Fn(&T) -> bool заменяет Func<T, bool>. Вместо генерации исключения возвращается Option.


Наследование против композиции

Наследование против композиции

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

Сложность: 🟡 Средний

// C# — наследование на основе классов
public abstract class Animal
{
    public string Name { get; protected set; }
    public abstract void MakeSound();
    
    public virtual void Sleep()
    {
        Console.WriteLine($"{Name} is sleeping");
    }
}

public class Dog : Animal
{
    public Dog(string name) { Name = name; }
    
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
    
    public void Fetch()
    {
        Console.WriteLine($"{Name} is fetching");
    }
}

// Контракты на основе интерфейсов
public interface IFlyable
{
    void Fly();
}

public class Bird : Animal, IFlyable
{
    public Bird(string name) { Name = name; }
    
    public override void MakeSound()
    {
        Console.WriteLine("Tweet!");
    }
    
    public void Fly()
    {
        Console.WriteLine($"{Name} is flying");
    }
}

Модель композиции в Rust

#![allow(unused)]
fn main() {
// Rust — композиция вместо наследования, через трейты
pub trait Animal {
    fn name(&self) -> &str;
    fn make_sound(&self);
    
    // Реализация по умолчанию (как виртуальные методы в C#)
    fn sleep(&self) {
        println!("{} is sleeping", self.name());
    }
}

pub trait Flyable {
    fn fly(&self);
}

// Отделяем данные от поведения
#[derive(Debug)]
pub struct Dog {
    name: String,
}

#[derive(Debug)]
pub struct Bird {
    name: String,
    wingspan: f64,
}

// Реализуем поведение для типов
impl Animal for Dog {
    fn name(&self) -> &str {
        &self.name
    }
    
    fn make_sound(&self) {
        println!("Woof!");
    }
}

impl Dog {
    pub fn new(name: String) -> Self {
        Dog { name }
    }
    
    pub fn fetch(&self) {
        println!("{} is fetching", self.name);
    }
}

impl Animal for Bird {
    fn name(&self) -> &str {
        &self.name
    }
    
    fn make_sound(&self) {
        println!("Tweet!");
    }
}

impl Flyable for Bird {
    fn fly(&self) {
        println!("{} is flying with {:.1}m wingspan", self.name, self.wingspan);
    }
}

// Несколько трейт-границ (как несколько интерфейсов)
fn make_flying_animal_sound<T>(animal: &T) 
where 
    T: Animal + Flyable,
{
    animal.make_sound();
    animal.fly();
}
}
graph TD
    subgraph "Иерархия наследования C#"
        CS_ANIMAL["Animal (абстрактный класс)"]
        CS_DOG["Dog : Animal"]
        CS_BIRD["Bird : Animal, IFlyable"]
        CS_VTABLE["Диспетчеризация виртуальных методов<br/>Затраты во время выполнения"]
        CS_COUPLING["[ERROR] Жёсткая связанность<br/>[ERROR] Ромбовидная проблема<br/>[ERROR] Глубокие иерархии"]
        
        CS_ANIMAL --> CS_DOG
        CS_ANIMAL --> CS_BIRD
        CS_DOG --> CS_VTABLE
        CS_BIRD --> CS_VTABLE
        CS_ANIMAL --> CS_COUPLING
    end
    
    subgraph "Модель композиции Rust"
        RUST_ANIMAL["trait Animal"]
        RUST_FLYABLE["trait Flyable"]
        RUST_DOG["struct Dog"]
        RUST_BIRD["struct Bird"]
        RUST_IMPL1["impl Animal for Dog"]
        RUST_IMPL2["impl Animal for Bird"]
        RUST_IMPL3["impl Flyable for Bird"]
        RUST_STATIC["Статическая диспетчеризация<br/>Нулевая стоимость"]
        RUST_FLEXIBLE["[OK] Гибкая композиция<br/>[OK] Нет ограничений иерархии<br/>[OK] Свободное комбинирование трейтов"]
        
        RUST_DOG --> RUST_IMPL1
        RUST_BIRD --> RUST_IMPL2
        RUST_BIRD --> RUST_IMPL3
        RUST_IMPL1 --> RUST_ANIMAL
        RUST_IMPL2 --> RUST_ANIMAL
        RUST_IMPL3 --> RUST_FLYABLE
        RUST_IMPL1 --> RUST_STATIC
        RUST_IMPL2 --> RUST_STATIC
        RUST_IMPL3 --> RUST_STATIC
        RUST_ANIMAL --> RUST_FLEXIBLE
        RUST_FLYABLE --> RUST_FLEXIBLE
    end
    
    style CS_COUPLING fill:#ffcdd2,color:#000
    style RUST_FLEXIBLE fill:#c8e6c9,color:#000
    style CS_VTABLE fill:#fff3e0,color:#000
    style RUST_STATIC fill:#c8e6c9,color:#000

Упражнения

🏋️ Упражнение: замените наследование трейтами (нажмите, чтобы раскрыть)

Этот C#-код использует наследование. Перепишите его на Rust, используя композицию трейтов:

public abstract class Shape { public abstract double Area(); }
public abstract class Shape3D : Shape { public abstract double Volume(); }
public class Cylinder : Shape3D
{
    public double Radius { get; }
    public double Height { get; }
    public Cylinder(double r, double h) { Radius = r; Height = h; }
    public override double Area() => 2.0 * Math.PI * Radius * (Radius + Height);
    public override double Volume() => Math.PI * Radius * Radius * Height;
}

Требования:

  1. Трейт HasArea с методом fn area(&self) -> f64
  2. Трейт HasVolume с методом fn volume(&self) -> f64
  3. Структура Cylinder, реализующая оба трейта
  4. Функция fn print_shape_info(shape: &(impl HasArea + HasVolume)) — обратите внимание на композицию трейт-границ (наследование не нужно)
🔑 Решение
use std::f64::consts::PI;

trait HasArea {
    fn area(&self) -> f64;
}

trait HasVolume {
    fn volume(&self) -> f64;
}

struct Cylinder {
    radius: f64,
    height: f64,
}

impl HasArea for Cylinder {
    fn area(&self) -> f64 {
        2.0 * PI * self.radius * (self.radius + self.height)
    }
}

impl HasVolume for Cylinder {
    fn volume(&self) -> f64 {
        PI * self.radius * self.radius * self.height
    }
}

fn print_shape_info(shape: &(impl HasArea + HasVolume)) {
    println!("Area:   {:.2}", shape.area());
    println!("Volume: {:.2}", shape.volume());
}

fn main() {
    let c = Cylinder { radius: 3.0, height: 5.0 };
    print_shape_info(&c);
}

Ключевая мысль: C# требует трёхуровневой иерархии (Shape → Shape3D → Cylinder). Rust использует плоскую композицию трейтов — impl HasArea + HasVolume объединяет возможности без глубины наследования.


11. Трейты From и Into

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

Что вы узнаете: трейты From/Into против неявных и явных операторов C#, TryFrom/TryInto для преобразований, которые могут завершиться ошибкой, FromStr для разбора строк, а также идиоматичные паттерны преобразования строк.

Сложность: 🟡 Средний

В C# используются неявные и явные преобразования и операторы приведения. В Rust для безопасных и явных преобразований применяются трейты From и Into.

Паттерны преобразований в C#

// Неявные и явные преобразования в C#
public class Temperature
{
    public double Celsius { get; }
    
    public Temperature(double celsius) { Celsius = celsius; }
    
    // Неявное преобразование
    public static implicit operator double(Temperature t) => t.Celsius;
    
    // Явное преобразование
    public static explicit operator Temperature(double d) => new Temperature(d);
}

double temp = new Temperature(100.0);  // неявно
Temperature t = (Temperature)37.5;     // явно

From и Into в Rust

#[derive(Debug)]
struct Temperature {
    celsius: f64,
}

impl From<f64> for Temperature {
    fn from(celsius: f64) -> Self {
        Temperature { celsius }
    }
}

impl From<Temperature> for f64 {
    fn from(temp: Temperature) -> f64 {
        temp.celsius
    }
}

fn main() {
    // From
    let temp = Temperature::from(100.0);
    
    // Into (доступен автоматически, когда реализован From)
    let temp2: Temperature = 37.5.into();
    
    // Работает и в аргументах функций
    fn process_temp(temp: impl Into<Temperature>) {
        let t: Temperature = temp.into();
        println!("Temperature: {:.1}°C", t.celsius);
    }
    
    process_temp(98.6);
    process_temp(Temperature { celsius: 0.0 });
}
graph LR
    A["impl From&lt;f64&gt; for Temperature"] -->|"генерирует автоматически"| B["impl Into&lt;Temperature&gt; for f64"]
    C["Temperature::from(37.5)"] -->|"явно"| D["Temperature"]
    E["37.5.into()"] -->|"неявно через Into"| D
    F["fn process(t: impl Into&lt;Temperature&gt;)"] -->|"принимает оба варианта"| D

    style A fill:#c8e6c9,color:#000
    style B fill:#bbdefb,color:#000

Эмпирическое правило: реализуйте From — и Into получите бесплатно. Вызывающий код может использовать тот вариант, который читается лучше.

TryFrom для преобразований, которые могут завершиться ошибкой

use std::convert::TryFrom;

impl TryFrom<i32> for Temperature {
    type Error = String;
    
    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value < -273 {
            Err(format!("Temperature {}°C is below absolute zero", value))
        } else {
            Ok(Temperature { celsius: value as f64 })
        }
    }
}

fn main() {
    match Temperature::try_from(-300) {
        Ok(t) => println!("Valid: {:?}", t),
        Err(e) => println!("Error: {}", e),
    }
}

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

#![allow(unused)]
fn main() {
// ToString через трейт Display
impl std::fmt::Display for Temperature {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:.1}°C", self.celsius)
    }
}

// Теперь .to_string() работает автоматически
let s = Temperature::from(100.0).to_string(); // "100.0°C"

// FromStr для разбора
use std::str::FromStr;

impl FromStr for Temperature {
    type Err = String;
    
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim_end_matches("°C").trim();
        let celsius: f64 = s.parse().map_err(|e| format!("Invalid temp: {}", e))?;
        Ok(Temperature { celsius })
    }
}

let t: Temperature = "100.0°C".parse().unwrap();
}

Упражнения

🏋️ Упражнение: конвертер валют (нажмите, чтобы раскрыть)

Создайте структуру Money, которая демонстрирует всю экосистему преобразований:

  1. Money { cents: i64 } (хранит значение в центах, чтобы избежать проблем с плавающей точкой)
  2. Реализуйте From<i64> (входное значение — целые доллары → cents = dollars * 100)
  3. Реализуйте TryFrom<f64> — отвергайте отрицательные суммы, округляйте до ближайшего цента
  4. Реализуйте Display, чтобы выводить формат "$1.50"
  5. Реализуйте FromStr, чтобы разбирать "$1.50" или "1.50" обратно в Money
  6. Напишите функцию fn total(items: &[impl Into<Money> + Copy]) -> Money, которая суммирует значения
🔑 Решение
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy)]
struct Money { cents: i64 }

impl From<i64> for Money {
    fn from(dollars: i64) -> Self {
        Money { cents: dollars * 100 }
    }
}

impl TryFrom<f64> for Money {
    type Error = String;
    fn try_from(value: f64) -> Result<Self, Self::Error> {
        if value < 0.0 {
            Err(format!("negative amount: {value}"))
        } else {
            Ok(Money { cents: (value * 100.0).round() as i64 })
        }
    }
}

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "${}.{:02}", self.cents / 100, self.cents.abs() % 100)
    }
}

impl FromStr for Money {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim_start_matches('$');
        let val: f64 = s.parse().map_err(|e| format!("{e}"))?;
        Money::try_from(val)
    }
}

fn main() {
    let a = Money::from(10);                       // $10.00
    let b = Money::try_from(3.50).unwrap();         // $3.50
    let c: Money = "$7.25".parse().unwrap();        // $7.25
    println!("{a} + {b} + {c}");
}

12. Замыкания и итераторы

Замыкания в Rust

Что вы узнаете: замыкания с захватом с учётом владения (Fn/FnMut/FnOnce) в сравнении с лямбдами C#, итераторы Rust как замену LINQ с нулевой стоимостью, ленивые и энергичные вычисления, а также параллельную итерацию с rayon.

Сложность: 🟡 Средний

Замыкания в Rust похожи на лямбды и делегаты C#, но захватывают переменные с учётом владения.

Лямбды и делегаты C#

// C# — лямбды захватывают по ссылке
Func<int, int> doubler = x => x * 2;
Action<string> printer = msg => Console.WriteLine(msg);

// Замыкание, захватывающее внешние переменные
int multiplier = 3;
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 15

// LINQ активно использует лямбды
var evens = numbers.Where(n => n % 2 == 0).ToList();

Замыкания в Rust

#![allow(unused)]
fn main() {
// Замыкания Rust — с учётом владения
let doubler = |x: i32| x * 2;
let printer = |msg: &str| println!("{}", msg);

// Замыкание, захватывающее по ссылке (по умолчанию для неизменяемых)
let multiplier = 3;
let multiply = |x: i32| x * multiplier; // заимствует multiplier
println!("{}", multiply(5)); // 15
println!("{}", multiplier); // по-прежнему доступен

// Замыкание, захватывающее перемещением
let data = vec![1, 2, 3];
let owns_data = move || {
    println!("{:?}", data); // data перемещается в замыкание
};
owns_data();
// println!("{:?}", data); // ОШИБКА: data было перемещено

// Использование замыканий с итераторами
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
}

Виды замыканий

// Fn — заимствует захваченные значения неизменяемо
fn apply_fn(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

// FnMut — заимствует захваченные значения изменяемо
fn apply_fn_mut(mut f: impl FnMut(i32), values: &[i32]) {
    for &v in values {
        f(v);
    }
}

// FnOnce — забирает владение захваченными значениями
fn apply_fn_once(f: impl FnOnce() -> Vec<i32>) -> Vec<i32> {
    f() // можно вызвать только один раз
}

fn main() {
    // Пример Fn
    let multiplier = 3;
    let result = apply_fn(|x| x * multiplier, 5);
    
    // Пример FnMut
    let mut sum = 0;
    apply_fn_mut(|x| sum += x, &[1, 2, 3, 4, 5]);
    println!("Sum: {}", sum); // 15
    
    // Пример FnOnce
    let data = vec![1, 2, 3];
    let result = apply_fn_once(move || data); // перемещает data
}

LINQ против итераторов Rust

LINQ в C# (Language Integrated Query)

// LINQ в C# — декларативная обработка данных
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var result = numbers
    .Where(n => n % 2 == 0)           // Отбираем чётные числа
    .Select(n => n * n)               // Возводим в квадрат
    .Where(n => n > 10)               // Отбираем > 10
    .OrderByDescending(n => n)        // Сортируем по убыванию
    .Take(3)                          // Берём первые 3
    .ToList();                        // Материализуем

// LINQ со сложными объектами
var users = GetUsers();
var activeAdults = users
    .Where(u => u.IsActive && u.Age >= 18)
    .GroupBy(u => u.Department)
    .Select(g => new {
        Department = g.Key,
        Count = g.Count(),
        AverageAge = g.Average(u => u.Age)
    })
    .OrderBy(x => x.Department)
    .ToList();

// Асинхронный LINQ (с дополнительными библиотеками)
var results = await users
    .ToAsyncEnumerable()
    .WhereAwait(async u => await IsActiveAsync(u.Id))
    .SelectAwait(async u => await EnrichUserAsync(u))
    .ToListAsync();

Итераторы Rust

#![allow(unused)]
fn main() {
// Итераторы Rust — ленивые абстракции с нулевой стоимостью
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let result: Vec<i32> = numbers
    .iter()
    .filter(|&&n| n % 2 == 0)        // Отбираем чётные числа
    .map(|&n| n * n)                 // Возводим в квадрат
    .filter(|&n| n > 10)             // Отбираем > 10
    .collect::<Vec<_>>()             // Собираем в Vec
    .into_iter()
    .rev()                           // Меняем порядок обхода
    .take(3)                         // Берём первые 3
    .collect();                      // Материализуем

// Сложные цепочки итераторов
use std::collections::HashMap;

#[derive(Debug, Clone)]
struct User {
    name: String,
    age: u32,
    department: String,
    is_active: bool,
}

fn process_users(users: Vec<User>) -> HashMap<String, (usize, f64)> {
    users
        .into_iter()
        .filter(|u| u.is_active && u.age >= 18)
        .fold(HashMap::new(), |mut acc, user| {
            let entry = acc.entry(user.department.clone()).or_insert((0, 0.0));
            entry.0 += 1;  // количество
            entry.1 += user.age as f64;  // сумма возрастов
            acc
        })
        .into_iter()
        .map(|(dept, (count, sum))| (dept, (count, sum / count as f64)))  // среднее
        .collect()
}

// Параллельная обработка с rayon
use rayon::prelude::*;

fn parallel_processing(numbers: Vec<i32>) -> Vec<i32> {
    numbers
        .par_iter()                  // Параллельный итератор
        .filter(|&&n| n % 2 == 0)
        .map(|&n| expensive_computation(n))
        .collect()
}

fn expensive_computation(n: i32) -> i32 {
    // Имитация тяжёлых вычислений
    (0..1000).fold(n, |acc, _| acc + 1)
}
}
graph TD
    subgraph "Особенности LINQ в C#"
        CS_LINQ["Выражение LINQ"]
        CS_EAGER["Часто энергичные вычисления<br/>(ToList(), ToArray())"]
        CS_REFLECTION["[ERROR] Часть рефлексии во время выполнения<br/>Деревья выражений"]
        CS_ALLOCATIONS["[ERROR] Промежуточные коллекции<br/>Нагрузка на сборщик мусора"]
        CS_ASYNC["[OK] Поддержка async<br/>(с дополнительными библиотеками)"]
        CS_SQL["[OK] Интеграция LINQ to SQL/EF"]
        
        CS_LINQ --> CS_EAGER
        CS_LINQ --> CS_REFLECTION
        CS_LINQ --> CS_ALLOCATIONS
        CS_LINQ --> CS_ASYNC
        CS_LINQ --> CS_SQL
    end
    
    subgraph "Особенности итераторов Rust"
        RUST_ITER["Цепочка итераторов"]
        RUST_LAZY["[OK] Ленивые вычисления<br/>Никакой работы до .collect()"]
        RUST_ZERO["[OK] Абстракции с нулевой стоимостью<br/>Компилируются в оптимальные циклы"]
        RUST_NO_ALLOC["[OK] Без промежуточных выделений<br/>Обработка на стеке"]
        RUST_PARALLEL["[OK] Простая параллелизация<br/>(крейт rayon)"]
        RUST_FUNCTIONAL["[OK] Функциональное программирование<br/>Неизменяемость по умолчанию"]
        
        RUST_ITER --> RUST_LAZY
        RUST_ITER --> RUST_ZERO
        RUST_ITER --> RUST_NO_ALLOC
        RUST_ITER --> RUST_PARALLEL
        RUST_ITER --> RUST_FUNCTIONAL
    end
    
    subgraph "Сравнение производительности"
        CS_PERF["Производительность LINQ в C#<br/>[ERROR] Накладные расходы на выделение памяти<br/>[ERROR] Виртуальная диспетчеризация<br/>[OK] Достаточно для большинства случаев"]
        RUST_PERF["Производительность итераторов Rust<br/>[OK] Скорость уровня ручной оптимизации<br/>[OK] Без выделений памяти<br/>[OK] Оптимизация на этапе компиляции"]
    end
    
    style CS_REFLECTION fill:#ffcdd2,color:#000
    style CS_ALLOCATIONS fill:#fff3e0,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000
    style RUST_LAZY fill:#c8e6c9,color:#000
    style RUST_NO_ALLOC fill:#c8e6c9,color:#000
    style CS_PERF fill:#fff3e0,color:#000
    style RUST_PERF fill:#c8e6c9,color:#000

🏋️ Упражнение: перевод из LINQ в итераторы (нажмите, чтобы раскрыть)

Задача: переведите этот конвейер LINQ на C# в идиоматичные итераторы Rust.

// C# — переведите на Rust
record Employee(string Name, string Dept, int Salary);

var result = employees
    .Where(e => e.Salary > 50_000)
    .GroupBy(e => e.Dept)
    .Select(g => new {
        Department = g.Key,
        Count = g.Count(),
        AvgSalary = g.Average(e => e.Salary)
    })
    .OrderByDescending(x => x.AvgSalary)
    .ToList();
🔑 Решение
#![allow(unused)]
fn main() {
use std::collections::HashMap;

struct Employee { name: String, dept: String, salary: u32 }

#[derive(Debug)]
struct DeptStats { department: String, count: usize, avg_salary: f64 }

fn department_stats(employees: &[Employee]) -> Vec<DeptStats> {
    let mut by_dept: HashMap<&str, Vec<u32>> = HashMap::new();
    for e in employees.iter().filter(|e| e.salary > 50_000) {
        by_dept.entry(&e.dept).or_default().push(e.salary);
    }

    let mut stats: Vec<DeptStats> = by_dept
        .into_iter()
        .map(|(dept, salaries)| {
            let count = salaries.len();
            let avg = salaries.iter().sum::<u32>() as f64 / count as f64;
            DeptStats { department: dept.to_string(), count, avg_salary: avg }
        })
        .collect();

    stats.sort_by(|a, b| b.avg_salary.partial_cmp(&a.avg_salary).unwrap());
    stats
}
}

Ключевые выводы:

  • В Rust нет встроенного group_by для итераторов — идиоматичный паттерн это HashMap + fold/for
  • Крейт itertools добавляет .group_by() для синтаксиса, более похожего на LINQ
  • Цепочки итераторов не имеют накладных расходов — компилятор превращает их в простые циклы

itertools: недостающие операции LINQ

Стандартные итераторы Rust покрывают map, filter, fold, take и collect. Но разработчики C#, привыкшие к GroupBy, Zip, Chunk, SelectMany и Distinct, сразу заметят пробелы. Крейт itertools их восполняет.

# Cargo.toml
[dependencies]
itertools = "0.12"

Бок о бок: LINQ и itertools

// C# — GroupBy
var byDept = employees.GroupBy(e => e.Department)
    .Select(g => new { Dept = g.Key, Count = g.Count() });

// C# — Chunk (пакетирование)
var batches = items.Chunk(100);  // IEnumerable<T[]>

// C# — Distinct / DistinctBy
var unique = users.DistinctBy(u => u.Email);

// C# — SelectMany (уплощение)
var allTags = posts.SelectMany(p => p.Tags);

// C# — Zip
var pairs = names.Zip(scores, (n, s) => new { Name = n, Score = s });

// C# — скользящее окно
var windows = data.Zip(data.Skip(1), data.Skip(2))
    .Select(triple => (triple.First + triple.Second + triple.Third) / 3.0);
#![allow(unused)]
fn main() {
use itertools::Itertools;

// Rust — group_by (требует отсортированный ввод)
let by_dept = employees.iter()
    .sorted_by_key(|e| &e.department)
    .group_by(|e| &e.department);
for (dept, group) in &by_dept {
    println!("{}: {} employees", dept, group.count());
}

// Rust — chunks (пакетирование)
let batches = items.iter().chunks(100);
for batch in &batches {
    process_batch(batch.collect::<Vec<_>>());
}

// Rust — unique / unique_by
let unique: Vec<_> = users.iter().unique_by(|u| &u.email).collect();

// Rust — flat_map (аналог SelectMany — встроен в std!)
let all_tags: Vec<&str> = posts.iter().flat_map(|p| &p.tags).collect();

// Rust — zip (встроен в std!)
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();

// Rust — tuple_windows (скользящее окно)
let moving_avg: Vec<f64> = data.iter()
    .tuple_windows::<(_, _, _)>()
    .map(|(a, b, c)| (*a + *b + *c) as f64 / 3.0)
    .collect();
}

Краткая справка по itertools

Метод LINQАналог в itertoolsПримечания
GroupBy(key).sorted_by_key().group_by()Требует отсортированный ввод (в отличие от LINQ)
Chunk(n).chunks(n)Возвращает итератор итераторов
Distinct().unique()Требует Eq + Hash
DistinctBy(key).unique_by(key)
SelectMany().flat_map()Встроен в std — крейт не нужен
Zip().zip()Встроен в std
Aggregate().fold()Встроен в std
Any() / All().any() / .all()Встроены в std
First() / Last().next() / .last()Встроены в std
Skip(n) / Take(n).skip(n) / .take(n)Встроены в std
OrderBy().sorted() / .sorted_by()itertools (в std сортировки нет)
ThenBy().sorted_by(|a,b| a.x.cmp(&b.x).then(a.y.cmp(&b.y)))Цепочка через Ordering::then
Intersect()Пересечение HashSetПрямого метода итератора нет
Concat().chain()Встроен в std
Скользящее окно.tuple_windows()Кортежи фиксированного размера
Декартово произведение.cartesian_product()itertools
Чередование.interleave()itertools
Перестановки.permutations(k)itertools

Пример из практики: конвейер анализа логов

#![allow(unused)]
fn main() {
use itertools::Itertools;
use std::collections::HashMap;

#[derive(Debug)]
struct LogEntry { level: String, module: String, message: String }

fn analyze_logs(entries: &[LogEntry]) {
    // Топ-5 самых «шумных» модулей (как LINQ GroupBy + OrderByDescending + Take)
    let noisy: Vec<_> = entries.iter()
        .into_group_map_by(|e| &e.module) // itertools: сразу группируем в HashMap
        .into_iter()
        .sorted_by(|a, b| b.1.len().cmp(&a.1.len()))
        .take(5)
        .collect();

    for (module, entries) in &noisy {
        println!("{}: {} entries", module, entries.len());
    }

    // Доля ошибок в окне из 100 записей (скользящее окно)
    let error_rates: Vec<f64> = entries.iter()
        .map(|e| if e.level == "ERROR" { 1.0 } else { 0.0 })
        .collect::<Vec<_>>()
        .windows(100)  // метод среза из std
        .map(|w| w.iter().sum::<f64>() / 100.0)
        .collect();

    // Удаляем подряд идущие одинаковые сообщения
    let deduped: Vec<_> = entries.iter().dedup_by(|a, b| a.message == b.message).collect();
    println!("Deduped {} → {} entries", entries.len(), deduped.len());
}
}

Введение в макросы

Макросы: код, который пишет код

Что вы узнаете: зачем Rust нужны макросы (нет перегрузки и вариативных аргументов), основы macro_rules!, соглашение о суффиксе !, распространённые производные макросы и dbg!() для быстрой отладки.

Сложность: 🟡 Средний

В C# нет прямого аналога макросам Rust. Понимание того, зачем они нужны и как работают, устраняет крупный источник путаницы для разработчиков C#.

Зачем в Rust нужны макросы

graph LR
    SRC["vec![1, 2, 3]"] -->|"на этапе компиляции"| EXP["{
  let mut v = Vec::new();
  v.push(1);
  v.push(2);
  v.push(3);
  v
}"]
    EXP -->|"компилируется в"| BIN["машинный код"]

    style SRC fill:#fff9c4,color:#000
    style EXP fill:#c8e6c9,color:#000
// В C# есть возможности, которые делают макросы ненужными:
Console.WriteLine("Hello");           // Перегрузка методов (1–16 параметров)
Console.WriteLine("{0}, {1}", a, b);  // Вариативность через массив params
var list = new List<int> { 1, 2, 3 }; // Синтаксис инициализатора коллекции
#![allow(unused)]
fn main() {
// В Rust НЕТ перегрузки функций, НЕТ вариативных аргументов, НЕТ особого синтаксиса.
// Макросы закрывают эти пробелы:
println!("Hello");                    // Макрос — принимает 0 и более аргументов на этапе компиляции
println!("{}, {}", a, b);             // Макрос — типы проверяются на этапе компиляции
let list = vec![1, 2, 3];            // Макрос — раскрывается в Vec::new() + push()
}

Распознавание макросов: суффикс !

Каждый вызов макроса заканчивается на !. Если вы видите !, это макрос, а не функция:

#![allow(unused)]
fn main() {
println!("hello");     // макрос — генерирует код форматирования на этапе компиляции
format!("{x}");        // макрос — возвращает String, проверка формата на этапе компиляции
vec![1, 2, 3];         // макрос — создаёт и заполняет Vec
todo!();               // макрос — паникует с сообщением "not yet implemented"
dbg!(expression);      // макрос — выводит файл:строку + выражение + значение, возвращает значение
assert_eq!(a, b);      // макрос — паникует с дифом, если a ≠ b
cfg!(target_os = "linux"); // макрос — определение платформы на этапе компиляции
}

Простой макрос на macro_rules!

// Определяем макрос, который создаёт HashMap из пар ключ-значение
macro_rules! hashmap {
    // Шаблон: пары ключ => значение, разделённые запятыми
    ( $( $key:expr => $value:expr ),* $(,)? ) => {{
        let mut map = std::collections::HashMap::new();
        $( map.insert($key, $value); )*
        map
    }};
}

fn main() {
    let scores = hashmap! {
        "Alice" => 100,
        "Bob"   => 85,
        "Carol" => 92,
    };
    println!("{scores:?}");
}

Производные макросы: автоматическая реализация трейтов

#![allow(unused)]
fn main() {
// #[derive] — это процедурный макрос, который генерирует реализации трейтов
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    name: String,
    age: u32,
}
// Компилятор автоматически генерирует Debug::fmt, Clone::clone, PartialEq::eq и т. д.,
// анализируя поля структуры.
}
// Аналога в C# нет — пришлось бы вручную реализовывать IEquatable, ICloneable и т. д.
// Или использовать records: public record User(string Name, int Age);
// Records автоматически генерируют Equals, GetHashCode, ToString — похожая идея!

Распространённые производные макросы

DeriveНазначениеАналог в C#
DebugВывод в формате {:?}Переопределение ToString()
CloneГлубокое копирование через .clone()ICloneable
CopyНеявное побитовое копирование (.clone() не нужен)Семантика значимого типа (struct)
PartialEq, EqСравнение ==IEquatable<T>
PartialOrd, OrdСравнение <, > и сортировкаIComparable<T>
HashХеширование для ключей HashMapGetHashCode()
DefaultЗначения по умолчанию через Default::default()Конструктор без параметров
Serialize, DeserializeJSON/TOML и т. д. (serde)Атрибуты [JsonProperty]

Эмпирическое правило: начинайте с #[derive(Debug)] на каждом типе. Добавляйте Clone, PartialEq, когда это нужно. Добавляйте Serialize, Deserialize для любого типа, который пересекает границу (API, файл, база данных).

Процедурные и атрибутные макросы (на уровне понимания)

Производные макросы — это один из видов процедурных макросов: кода, который выполняется на этапе компиляции и генерирует код. Вы встретите ещё две формы:

Атрибутные макросы — присоединяются к элементам через #[...]:

#[tokio::main]          // превращает main() в точку входа асинхронного рантайма
async fn main() { }

#[test]                 // помечает функцию как модульный тест
fn it_works() { assert_eq!(2 + 2, 4); }

#[cfg(test)]            // условно компилирует этот модуль только во время тестирования
mod tests { /* ... */ }

Функциональные макросы — выглядят как вызовы функций:

#![allow(unused)]
fn main() {
// sqlx::query! проверяет ваш SQL по базе данных на этапе компиляции
let users = sqlx::query!("SELECT id, name FROM users WHERE active = $1", true)
    .fetch_all(&pool)
    .await?;
}

Ключевая мысль для разработчиков C#: процедурные макросы вы редко пишете — это инструмент для авторов библиотек продвинутого уровня. Но вы используете их постоянно (#[derive(...)], #[tokio::main], #[test]). Думайте о них как об источниках генерации кода (source generators) в C#: вы получаете выгоду, не реализуя их сами.

Условная компиляция через #[cfg]

Атрибуты #[cfg] в Rust похожи на директивы препроцессора #if DEBUG в C#, но с проверкой типов:

#![allow(unused)]
fn main() {
// Компилируем эту функцию только для Linux
#[cfg(target_os = "linux")]
fn platform_specific() {
    println!("Running on Linux");
}

// Проверки только для отладки (как Debug.Assert в C#)
#[cfg(debug_assertions)]
fn expensive_check(data: &[u8]) {
    assert!(data.len() < 1_000_000, "data unexpectedly large");
}

// Feature-флаги (как #if FEATURE_X в C#, но объявляются в Cargo.toml)
#[cfg(feature = "json")]
pub fn to_json<T: Serialize>(val: &T) -> String {
    serde_json::to_string(val).unwrap()
}
}
// Аналог в C#
#if DEBUG
    Debug.Assert(data.Length < 1_000_000);
#endif

dbg!() — ваш лучший друг при отладке

#![allow(unused)]
fn main() {
fn calculate(x: i32) -> i32 {
    let intermediate = dbg!(x * 2);     // выводит: [src/main.rs:3] x * 2 = 10
    let result = dbg!(intermediate + 1); // выводит: [src/main.rs:4] intermediate + 1 = 11
    result
}
// dbg! пишет в stderr, включает файл:строку и возвращает значение
// Гораздо полезнее, чем Console.WriteLine для отладки!
}
🏋️ Упражнение: напишите макрос min! (нажмите, чтобы раскрыть)

Задача: напишите макрос min!, который принимает два или более аргументов и возвращает наименьший.

#![allow(unused)]
fn main() {
// Должно работать так:
let smallest = min!(5, 3, 8, 1, 4); // → 1
let pair = min!(10, 20);             // → 10
}
🔑 Решение
macro_rules! min {
    // Базовый случай: одно значение
    ($x:expr) => ($x);
    // Рекурсивно: сравниваем первое со минимумом остальных
    ($x:expr, $($rest:expr),+) => {{
        let first = $x;
        let rest = min!($($rest),+);
        if first < rest { first } else { rest }
    }};
}

fn main() {
    assert_eq!(min!(5, 3, 8, 1, 4), 1);
    assert_eq!(min!(10, 20), 10);
    assert_eq!(min!(42), 42);
    println!("All assertions passed!");
}

Ключевая мысль: macro_rules! выполняет сопоставление с образцом по деревьям токенов — это как match, только для структуры кода, а не значений.


13. Конкурентность

Потокобезопасность: соглашение против гарантий системы типов

Что вы узнаете: как Rust обеспечивает потокобезопасность на этапе компиляции в отличие от подхода C# на основе соглашений, Arc<Mutex<T>> против lock, каналы против ConcurrentQueue, трейты Send/Sync, ограниченные по области потоки и переход к async/await.

Сложность: 🔴 Продвинутый

Глубокое погружение: о производственных асинхронных паттернах (обработка потоков, корректное завершение, пулы соединений, безопасность отмены) см. сопутствующее руководство Async Rust Training.

Предварительные требования: Владение и заимствование и Умные указатели (дерево решений Rc против Arc).

C# — потокобезопасность по соглашению

// Коллекции в C# по умолчанию не потокобезопасны
public class UserService
{
    private readonly List<string> items = new();
    private readonly Dictionary<int, User> cache = new();

    // Это может привести к гонкам данных:
    public void AddItem(string item)
    {
        items.Add(item);  // Не потокобезопасно!
    }

    // Приходится вручную использовать блокировки:
    private readonly object lockObject = new();

    public void SafeAddItem(string item)
    {
        lock (lockObject)
        {
            items.Add(item);  // Безопасно, но есть накладные расходы рантайма
        }
        // Легко забыть о блокировке в другом месте
    }

    // ConcurrentCollection помогает, но возможности ограничены
    private readonly ConcurrentBag<string> safeItems = new();
    
    public void ConcurrentAdd(string item)
    {
        safeItems.Add(item);  // Потокобезопасно, но операции ограничены
    }

    // Сложное управление разделяемым состоянием
    private readonly ConcurrentDictionary<int, User> threadSafeCache = new();
    private volatile bool isShutdown = false;
    
    public async Task ProcessUser(int userId)
    {
        if (isShutdown) return;  // Возможна гонка!
        
        var user = await GetUser(userId);
        threadSafeCache.TryAdd(userId, user);  // Нужно помнить, какие коллекции безопасны
    }

    // Локальное для потока хранилище требует аккуратного управления
    private static readonly ThreadLocal<Random> threadLocalRandom = 
        new ThreadLocal<Random>(() => new Random());
        
    public int GetRandomNumber()
    {
        return threadLocalRandom.Value.Next();  // Безопасно, но управление вручную
    }
}

// Обработка событий с возможными гонками
public class EventProcessor
{
    public event Action<string> DataReceived;
    private readonly List<string> eventLog = new();
    
    public void OnDataReceived(string data)
    {
        // Гонка: обработчик может стать null между проверкой и вызовом
        if (DataReceived != null)
        {
            DataReceived(data);
        }
        // Современный C# (6+) смягчает гонку на null через: DataReceived?.Invoke(data);
        // но модель событий-делегатов по-прежнему допускает гонки на списке ниже
        
        // Ещё одна гонка — список не потокобезопасен
        eventLog.Add($"Processed: {data}");
    }
}

Rust — потокобезопасность гарантируется системой типов

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::collections::HashMap;
use tokio::sync::{mpsc, broadcast};

// Rust предотвращает гонки данных на этапе компиляции
pub struct UserService {
    items: Arc<Mutex<Vec<String>>>,
    cache: Arc<RwLock<HashMap<i32, User>>>,
}

impl UserService {
    pub fn new() -> Self {
        UserService {
            items: Arc::new(Mutex::new(Vec::new())),
            cache: Arc::new(RwLock::new(HashMap::new())),
        }
    }
    
    pub fn add_item(&self, item: String) {
        let mut items = self.items.lock().unwrap();
        items.push(item);
        // Блокировка автоматически освобождается, когда `items` выходит из области видимости
    }
    
    // Несколько читателей, один писатель — обеспечивается автоматически
    pub async fn get_user(&self, user_id: i32) -> Option<User> {
        let cache = self.cache.read().unwrap();
        cache.get(&user_id).cloned()
    }
    
    pub async fn cache_user(&self, user_id: i32, user: User) {
        let mut cache = self.cache.write().unwrap();
        cache.insert(user_id, user);
    }
    
    // Клонируем Arc для совместного использования между потоками
    pub fn process_in_background(&self) {
        let items = Arc::clone(&self.items);
        
        thread::spawn(move || {
            let items = items.lock().unwrap();
            for item in items.iter() {
                println!("Processing: {}", item);
            }
        });
    }
}

// Взаимодействие через каналы — разделяемое состояние не нужно
pub struct MessageProcessor {
    sender: mpsc::UnboundedSender<String>,
}

impl MessageProcessor {
    pub fn new() -> (Self, mpsc::UnboundedReceiver<String>) {
        let (tx, rx) = mpsc::unbounded_channel();
        (MessageProcessor { sender: tx }, rx)
    }
    
    pub fn send_message(&self, message: String) -> Result<(), mpsc::error::SendError<String>> {
        self.sender.send(message)
    }
}

// Этот код не скомпилируется — Rust не даёт небезопасно разделять изменяемые данные:
fn impossible_data_race() {
    let mut items = vec![1, 2, 3];
    
    // Не скомпилируется — нельзя переместить `items` в несколько замыканий
    /*
    thread::spawn(move || {
        items.push(4);  // ОШИБКА: использование перемещённого значения
    });
    
    thread::spawn(move || {
        items.push(5);  // ОШИБКА: использование перемещённого значения
    });
    */
}

// Безопасная параллельная обработка данных
use rayon::prelude::*;

fn parallel_processing() {
    let data = vec![1, 2, 3, 4, 5];
    
    // Параллельная итерация — потокобезопасность гарантирована
    let results: Vec<i32> = data
        .par_iter()
        .map(|&x| x * x)
        .collect();
        
    println!("{:?}", results);
}

// Асинхронная конкурентность с передачей сообщений
async fn async_message_passing() {
    let (tx, mut rx) = mpsc::channel(100);
    
    // Задача-производитель
    let producer = tokio::spawn(async move {
        for i in 0..10 {
            if tx.send(i).await.is_err() {
                break;
            }
        }
    });
    
    // Задача-потребитель
    let consumer = tokio::spawn(async move {
        while let Some(value) = rx.recv().await {
            println!("Received: {}", value);
        }
    });
    
    // Ждём обе задачи
    let (producer_result, consumer_result) = tokio::join!(producer, consumer);
    producer_result.unwrap();
    consumer_result.unwrap();
}

#[derive(Clone)]
struct User {
    id: i32,
    name: String,
}
}
graph TD
    subgraph "Проблемы потокобезопасности в C#"
        CS_MANUAL["Ручная синхронизация"]
        CS_LOCKS["Инструкции lock"]
        CS_CONCURRENT["Потокобезопасные коллекции"]
        CS_VOLATILE["Поля volatile"]
        CS_FORGET["😰 Легко забыть блокировки"]
        CS_DEADLOCK["💀 Возможны взаимные блокировки"]
        CS_RACE["🏃 Гонки данных"]
        CS_OVERHEAD["⚡ Накладные расходы рантайма"]
        
        CS_MANUAL --> CS_LOCKS
        CS_MANUAL --> CS_CONCURRENT
        CS_MANUAL --> CS_VOLATILE
        CS_LOCKS --> CS_FORGET
        CS_LOCKS --> CS_DEADLOCK
        CS_FORGET --> CS_RACE
        CS_LOCKS --> CS_OVERHEAD
    end
    
    subgraph "Гарантии системы типов Rust"
        RUST_OWNERSHIP["Система владения"]
        RUST_BORROWING["Проверщик заимствований"]
        RUST_SEND["Трейт Send"]
        RUST_SYNC["Трейт Sync"]
        RUST_ARC["Arc<Mutex<T>>"]
        RUST_CHANNELS["Передача сообщений"]
        RUST_SAFE["✅ Гонки данных невозможны"]
        RUST_FAST["⚡ Абстракции с нулевой стоимостью"]
        
        RUST_OWNERSHIP --> RUST_BORROWING
        RUST_BORROWING --> RUST_SEND
        RUST_SEND --> RUST_SYNC
        RUST_SYNC --> RUST_ARC
        RUST_ARC --> RUST_CHANNELS
        RUST_CHANNELS --> RUST_SAFE
        RUST_SAFE --> RUST_FAST
    end
    
    style CS_FORGET fill:#ffcdd2,color:#000
    style CS_DEADLOCK fill:#ffcdd2,color:#000
    style CS_RACE fill:#ffcdd2,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000
    style RUST_FAST fill:#c8e6c9,color:#000

🏋️ Упражнение: потокобезопасный счётчик (нажмите, чтобы раскрыть)

Задача: реализуйте потокобезопасный счётчик, который могут одновременно инкрементировать 10 потоков. Каждый поток увеличивает его 1000 раз. Итоговое значение должно быть ровно 10 000.

🔑 Решение
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0u64));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let mut count = counter.lock().unwrap();
                *count += 1;
            }
        }));
    }

    for h in handles { h.join().unwrap(); }
    assert_eq!(*counter.lock().unwrap(), 10_000);
    println!("Final count: {}", counter.lock().unwrap());
}

Или с атомарными типами (быстрее, без блокировок):

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(AtomicU64::new(0));
    let handles: Vec<_> = (0..10).map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            for _ in 0..1000 {
                counter.fetch_add(1, Ordering::Relaxed);
            }
        })
    }).collect();

    for h in handles { h.join().unwrap(); }
    assert_eq!(counter.load(Ordering::SeqCst), 10_000);
}

Ключевая мысль: Arc<Mutex<T>> — общий паттерн. Для простых счётчиков AtomicU64 полностью избавляет от накладных расходов на блокировки.

Почему Rust предотвращает гонки данных: Send и Sync

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

  • Send: тип можно безопасно передать в другой поток (например, переместить в замыкание, переданное в thread::spawn)
  • Sync: тип можно безопасно разделять (через &T) между потоками

Большинство типов автоматически реализуют Send + Sync. Заметные исключения:

  • Rc<T> не реализует ни Send, ни Sync — компилятор не даст передать его в thread::spawn (используйте Arc<T>)
  • Cell<T> и RefCell<T> не реализуют Sync — для потокобезопасной внутренней изменяемости используйте Mutex<T> или RwLock<T>
  • Сырые указатели (*const T, *mut T) не реализуют ни Send, ни Sync

В C# List<T> не потокобезопасен, но компилятор не помешает разделить его между потоками. В Rust такая же ошибка — это ошибка компиляции, а не гонка во время выполнения.

Ограниченные по области потоки: заимствование из стека

thread::scope() позволяет порождённым потокам заимствовать локальные переменные — Arc не нужен:

use std::thread;

fn main() {
    let data = vec![1, 2, 3, 4, 5];
    
    // Потоки в области видимости могут заимствовать 'data' — scope ждёт завершения всех потоков
    thread::scope(|s| {
        s.spawn(|| println!("Thread 1: {data:?}"));
        s.spawn(|| println!("Thread 2: sum = {}", data.iter().sum::<i32>()));
    });
    // 'data' здесь по-прежнему действителен — потоки гарантированно завершились
}

Это похоже на Parallel.ForEach в C# тем, что вызывающий код ждёт завершения, но проверщик заимствований Rust доказывает отсутствие гонок данных на этапе компиляции.

Переход к async/await

Разработчики C# обычно используют Task и async/await, а не потоки напрямую. В Rust есть обе парадигмы:

C#RustКогда использовать
Threadstd::thread::spawnЗадачи, ограниченные CPU; один поток ОС на задачу
Task.Runtokio::spawnАсинхронная задача в рантайме
async/awaitasync/awaitКонкурентность, ограниченная вводом-выводом
lockMutex<T>Синхронная взаимная блокировка
SemaphoreSlimtokio::sync::SemaphoreОграничение асинхронной конкурентности
Interlockedstd::sync::atomicАтомарные операции без блокировок
CancellationTokentokio_util::sync::CancellationTokenКооперативная отмена

Следующая глава (Глубокое погружение в async/await) подробно описывает асинхронную модель Rust — в том числе то, чем она отличается от модели C# на основе Task.

Глубокое погружение в async/await

Асинхронное программирование: Task в C# против Future в Rust

Что вы узнаете: ленивый Future Rust против энергичного Task в C#, модель исполнителя (tokio), отмену через Drop + select! против CancellationToken, а также практические паттерны для конкурентных запросов.

Сложность: 🔴 Продвинутый

Разработчики C# хорошо знакомы с async/await. Rust использует те же ключевые слова, но модель выполнения принципиально иная.

Модель исполнителя

// C# — рантайм предоставляет встроенный пул потоков и планировщик задач
// async/await «просто работает» из коробки
public async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);  // Планируется пулом потоков .NET
}
// .NET управляет пулом потоков, планированием задач и контекстом синхронизации
// Rust — встроенного асинхронного рантайма нет. Исполнитель выбираете вы.
// Самый популярный — tokio.
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

// Чтобы выполнить асинхронный код, нужен рантайм:
#[tokio::main]  // Этот макрос настраивает рантайм tokio
async fn main() {
    let data = fetch_data("https://example.com").await.unwrap();
    println!("{}", &data[..100]);
}

Future против Task

C# Task<T>Rust Future<Output = T>
ВыполнениеЗапускается сразу при созданииЛенивый — ничего не делает, пока его не await-ят
РантаймВстроенный (пул потоков CLR)Внешний (tokio, async-std и т. д.)
ОтменаCancellationTokenУничтожить Future (или tokio::select!)
Конечный автоматГенерируется компиляторомГенерируется компилятором
РазмещениеВ кучеНа стеке, пока не упаковано в Box
#![allow(unused)]
fn main() {
// ВАЖНО: Future в Rust ленивые!
async fn compute() -> i32 { println!("Computing!"); 42 }

let future = compute();  // Ничего не выведено! Future ещё не опрошен.
let result = future.await; // ТЕПЕРЬ выводится "Computing!"
}
// Task в C# запускаются сразу!
var task = ComputeAsync();  // "Computing!" выводится немедленно
var result = await task;    // Просто ждёт завершения

Отмена: CancellationToken против Drop / select!

// C# — кооперативная отмена через CancellationToken
public async Task ProcessAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await Task.Delay(1000, ct);  // Бросает исключение, если отменено
        DoWork();
    }
}

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await ProcessAsync(cts.Token);
#![allow(unused)]
fn main() {
// Rust — отмена через уничтожение future или через tokio::select!
use tokio::time::{sleep, Duration};

async fn process() {
    loop {
        sleep(Duration::from_secs(1)).await;
        do_work();
    }
}

// Паттерн таймаута с select!
async fn run_with_timeout() {
    tokio::select! {
        _ = process() => { println!("Completed"); }
        _ = sleep(Duration::from_secs(5)) => { println!("Timed out!"); }
    }
    // Когда select! выбирает ветку таймаута, future process() УНИЧТОЖАЕТСЯ
    // — автоматическая очистка, CancellationToken не нужен
}
}

Практический паттерн: конкурентные запросы с таймаутом

// C# — конкурентные HTTP-запросы с таймаутом
public async Task<string[]> FetchAllAsync(string[] urls, CancellationToken ct)
{
    var tasks = urls.Select(url => httpClient.GetStringAsync(url, ct));
    return await Task.WhenAll(tasks);
}
#![allow(unused)]
fn main() {
// Rust — конкурентные запросы через tokio::join! или futures::join_all
use futures::future::join_all;

async fn fetch_all(urls: &[&str]) -> Vec<Result<String, reqwest::Error>> {
    let futures = urls.iter().map(|url| reqwest::get(*url));
    let responses = join_all(futures).await;

    let mut results = Vec::new();
    for resp in responses {
        results.push(resp?.text().await);
    }
    results
}

// С таймаутом:
async fn fetch_all_with_timeout(urls: &[&str]) -> Result<Vec<String>, &'static str> {
    tokio::time::timeout(
        Duration::from_secs(10),
        async {
            let futures: Vec<_> = urls.iter()
                .map(|url| async { reqwest::get(*url).await?.text().await })
                .collect();
            let results = join_all(futures).await;
            results.into_iter().collect::<Result<Vec<_>, _>>()
        }
    )
    .await
    .map_err(|_| "Request timed out")?
    .map_err(|_| "Request failed")
}
}
🏋️ Упражнение: паттерн асинхронного таймаута (нажмите, чтобы раскрыть)

Задача: напишите асинхронную функцию, которая запрашивает два URL конкурентно, возвращает тот результат, который пришёл первым, а второй запрос отменяет. (В C# это Task.WhenAny.)

🔑 Решение
use tokio::time::{sleep, Duration};

// Имитация асинхронного запроса
async fn fetch(url: &str, delay_ms: u64) -> String {
    sleep(Duration::from_millis(delay_ms)).await;
    format!("Response from {url}")
}

async fn fetch_first(url1: &str, url2: &str) -> String {
    tokio::select! {
        result = fetch(url1, 200) => {
            println!("URL 1 won");
            result
        }
        result = fetch(url2, 500) => {
            println!("URL 2 won");
            result
        }
    }
    // Future проигравшей ветки автоматически уничтожается (отменяется)
}

#[tokio::main]
async fn main() {
    let result = fetch_first("https://fast.api", "https://slow.api").await;
    println!("{result}");
}

Ключевая мысль: tokio::select! — это аналог Task.WhenAny в Rust. Он гонит несколько future, завершается, когда первое из них готово, и уничтожает (отменяет) остальные.

Запуск независимых задач через tokio::spawn

В C# Task.Run запускает работу, которая выполняется независимо от вызывающего кода. Аналог в Rust — tokio::spawn:

#![allow(unused)]
fn main() {
use tokio::task;

async fn background_work() {
    // Выполняется независимо — даже если future вызывающего кода уничтожен
    let handle = task::spawn(async {
        tokio::time::sleep(Duration::from_secs(2)).await;
        42
    });

    // Делаем другую работу, пока порождённая задача выполняется...
    println!("Doing other work");

    // Ждём результат, когда он понадобится
    let result = handle.await.unwrap(); // 42
}
}
// Аналог в C#
var task = Task.Run(async () => {
    await Task.Delay(2000);
    return 42;
});
// Делаем другую работу...
var result = await task;

Ключевое отличие: обычный блок async {} ленивый — он ничего не делает, пока его не await-ят. tokio::spawn сразу запускает его в рантайме, как Task.Run в C#.

Pin: понятие, которого в C# нет

Разработчики C# никогда не сталкиваются с Pin — сборщик мусора CLR свободно перемещает объекты и автоматически обновляет все ссылки. В Rust сборщика мусора нет. Когда компилятор превращает async fn в конечный автомат, эта структура может содержать внутренние указатели на собственные поля. Перемещение структуры сделало бы эти указатели недействительными.

Pin<T> — это обёртка, которая говорит: «это значение не будет перемещено в памяти».

#![allow(unused)]
fn main() {
// Pin встречается в таких контекстах:
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
    //           ^^^^^^^^^^^^^^ закреплено — внутренние ссылки остаются действительными
}

// Возврат future в упаковке Box из трейта:
fn make_future() -> Pin<Box<dyn Future<Output = i32> + Send>> {
    Box::pin(async { 42 })
}
}

На практике вы почти никогда не пишете Pin сами. Синтаксис async fn и .await справляется с этим. С Pin вы столкнётесь только в таких случаях:

  • Сообщения об ошибках компилятора (следуйте подсказке)
  • tokio::select! (используйте макрос pin!())
  • Методы трейтов, возвращающие dyn Future (используйте Box::pin(async { ... }))

Хотите глубже? Сопутствующее руководство Async Rust Training подробно рассматривает Pin, Unpin, самоссылающиеся структуры и структурное закрепление.


14. Unsafe Rust и FFI

Небезопасный Rust (unsafe Rust)

Что вы узнаете: что разрешает unsafe (сырые указатели, FFI, непроверяемые приведения), паттерны безопасных обёрток, P/Invoke в C# против FFI в Rust для вызова нативного кода, а также чек-лист безопасности для блоков unsafe.

Сложность: 🔴 Продвинутый

Unsafe Rust позволяет выполнять операции, которые проверщик заимствований не может верифицировать. Используйте его осторожно и с понятной документацией.

Расширенное описание: о безопасных абстракциях поверх unsafe-кода (арена-аллокаторы, структуры без блокировок, пользовательские таблицы виртуальных методов) см. Паттерны Rust.

Когда нужен unsafe

#![allow(unused)]
fn main() {
// 1. Разыменование сырых указателей
let mut value = 42;
let ptr = &mut value as *mut i32;
// SAFETY: ptr указывает на действительную, живую локальную переменную.
unsafe {
    *ptr = 100; // Должно находиться в блоке unsafe
}

// 2. Вызов небезопасных функций
unsafe fn dangerous() {
    // Внутренняя реализация, которая требует от вызывающего кода соблюдения инвариантов
}

// SAFETY: для этой примерной функции инвариантов нет.
unsafe {
    dangerous(); // Ответственность берёт на себя вызывающий код
}

// 3. Доступ к изменяемым статическим переменным
static mut COUNTER: u32 = 0;
// SAFETY: однопоточный контекст; параллельного доступа к COUNTER нет.
unsafe {
    COUNTER += 1; // Не потокобезопасно — вызывающий код должен обеспечить синхронизацию
}

// 4. Реализация небезопасных трейтов
unsafe trait UnsafeTrait {
    fn do_something(&self);
}
}

Сравнение с C#: ключевое слово unsafe

// unsafe в C# — похожая идея, но другой масштаб
unsafe void UnsafeExample()
{
    int value = 42;
    int* ptr = &value;
    *ptr = 100;
    
    // unsafe в C# — это про арифметику указателей
    // unsafe в Rust — это про ослабление правил владения и заимствования
}

// fixed в C# — закрепление управляемых объектов
unsafe void PinnedExample()
{
    byte[] buffer = new byte[100];
    fixed (byte* ptr = buffer)
    {
        // ptr действителен только внутри этого блока
    }
}

Безопасные обёртки

#![allow(unused)]
fn main() {
/// Ключевой паттерн: оборачиваем небезопасный код в безопасный API
pub struct SafeBuffer {
    data: Vec<u8>,
}

impl SafeBuffer {
    pub fn new(size: usize) -> Self {
        SafeBuffer { data: vec![0; size] }
    }
    
    /// Безопасный API — доступ с проверкой границ
    pub fn get(&self, index: usize) -> Option<u8> {
        self.data.get(index).copied()
    }
    
    /// Быстрый доступ без проверки — небезопасный, но безопасно обёрнутый проверкой границ
    pub fn get_unchecked_safe(&self, index: usize) -> Option<u8> {
        if index < self.data.len() {
            // SAFETY: мы только что проверили, что index находится в границах
            Some(unsafe { *self.data.get_unchecked(index) })
        } else {
            None
        }
    }
}
}

Взаимодействие с C# через FFI

Rust может экспортировать функции с C-совместимым интерфейсом, которые C# вызывает через P/Invoke.

graph LR
    subgraph "Процесс C#"
        CS["Код C#"] -->|"P/Invoke"| MI["Слой маршалинга<br/>UTF-16 → UTF-8<br/>раскладка структур"]
    end
    MI -->|"Вызов по C ABI"| FFI["Граница FFI"]
    subgraph "Rust cdylib (.so / .dll)"
        FFI --> RF["extern #quot;C#quot; fn<br/>#[no_mangle]"]
        RF --> Safe["Безопасные внутренности<br/>на Rust"]
    end

    style FFI fill:#fff9c4,color:#000
    style MI fill:#bbdefb,color:#000
    style Safe fill:#c8e6c9,color:#000

Библиотека Rust (собирается как cdylib)

#![allow(unused)]
fn main() {
// src/lib.rs
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn process_string(input: *const std::os::raw::c_char) -> i32 {
    // SAFETY: input проверяется на null внутри, а вызывающий код гарантирует null-терминированность.
    let c_str = unsafe {
        if input.is_null() {
            return -1;
        }
        std::ffi::CStr::from_ptr(input)
    };
    
    match c_str.to_str() {
        Ok(s) => s.len() as i32,
        Err(_) => -1,
    }
}
}
# Cargo.toml
[lib]
crate-type = ["cdylib"]

Потребитель на C# (P/Invoke)

using System.Runtime.InteropServices;

public static class RustInterop
{
    [DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
    public static extern int add_numbers(int a, int b);
    
    [DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
    public static extern int process_string(
        [MarshalAs(UnmanagedType.LPUTF8Str)] string input);
}

// Использование
int sum = RustInterop.add_numbers(5, 3);  // 8
int len = RustInterop.process_string("Hello from C#!");  // 15

Чек-лист безопасности FFI

При экспорте функций Rust в C# эти правила предотвращают большинство частых ошибок:

  1. Всегда используйте extern "C" — без него Rust применяет собственное (нестабильное) соглашение о вызовах. P/Invoke в C# ожидает C ABI.

  2. #[no_mangle] — не даёт компилятору Rust менять имя функции. Без него C# не сможет найти символ.

  3. Никогда не пускайте панику через границу FFI — раскрутка паники Rust внутрь C# является неопределённым поведением. Перехватывайте паники на входах в FFI:

    #![allow(unused)]
    fn main() {
    #[no_mangle]
    pub extern "C" fn safe_ffi_function() -> i32 {
        match std::panic::catch_unwind(|| {
            // реальная логика здесь
            42
        }) {
            Ok(result) => result,
            Err(_) => -1,  // Возвращаем код ошибки вместо паники внутрь C#
        }
    }
    }
  4. Непрозрачные и прозрачные структуры — если C# хранит только указатель (непрозрачный дескриптор), #[repr(C)] не нужен. Если C# читает поля структуры через StructLayout, #[repr(C)] обязателен:

    #![allow(unused)]
    fn main() {
    // Непрозрачная — C# хранит только IntPtr. #[repr(C)] не нужен.
    pub struct Connection { /* поля только для Rust */ }
    
    // Прозрачная — C# маршалирует поля напрямую. ОБЯЗАТЕЛЬНО #[repr(C)].
    #[repr(C)]
    pub struct Point { pub x: f64, pub y: f64 }
    }
  5. Проверки на null-указатели — всегда проверяйте указатели перед разыменованием. C# может передать IntPtr.Zero.

  6. Кодировка строк — C# использует UTF-16 внутренне. MarshalAs(UnmanagedType.LPUTF8Str) конвертирует строку в UTF-8 для CStr в Rust. Этот контракт нужно явно документировать.

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

Этот паттерн распространён в продакшене: Rust владеет объектом, C# хранит непрозрачный дескриптор, а явные функции создания и уничтожения управляют жизненным циклом.

Сторона Rust (src/lib.rs):

#![allow(unused)]
fn main() {
use std::ffi::{c_char, CStr};

pub struct ImageProcessor {
    width: u32,
    height: u32,
    pixels: Vec<u8>,
}

/// Создаёт новый обработчик. Возвращает null при некорректных размерах.
#[no_mangle]
pub extern "C" fn processor_new(width: u32, height: u32) -> *mut ImageProcessor {
    if width == 0 || height == 0 {
        return std::ptr::null_mut();
    }
    let proc = ImageProcessor {
        width,
        height,
        pixels: vec![0u8; (width * height * 4) as usize],
    };
    Box::into_raw(Box::new(proc)) // Выделяем в куче, возвращаем сырой указатель
}

/// Применяет фильтр оттенков серого. Возвращает 0 при успехе, -1 при null-указателе.
#[no_mangle]
pub extern "C" fn processor_grayscale(ptr: *mut ImageProcessor) -> i32 {
    // SAFETY: ptr был создан через Box::into_raw (не null), всё ещё действителен.
    let proc = match unsafe { ptr.as_mut() } {
        Some(p) => p,
        None => return -1,
    };
    for chunk in proc.pixels.chunks_exact_mut(4) {
        let gray = (0.299 * chunk[0] as f64
                  + 0.587 * chunk[1] as f64
                  + 0.114 * chunk[2] as f64) as u8;
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
    }
    0
}

/// Уничтожает обработчик. Безопасно вызывать с null.
#[no_mangle]
pub extern "C" fn processor_free(ptr: *mut ImageProcessor) {
    if !ptr.is_null() {
        // SAFETY: ptr был создан через processor_new с помощью Box::into_raw
        unsafe { drop(Box::from_raw(ptr)); }
    }
}
}

Сторона C#:

using System.Runtime.InteropServices;

public sealed class ImageProcessor : IDisposable
{
    [DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr processor_new(uint width, uint height);

    [DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
    private static extern int processor_grayscale(IntPtr ptr);

    [DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
    private static extern void processor_free(IntPtr ptr);

    private IntPtr _handle;

    public ImageProcessor(uint width, uint height)
    {
        _handle = processor_new(width, height);
        if (_handle == IntPtr.Zero)
            throw new ArgumentException("Invalid dimensions");
    }

    public void Grayscale()
    {
        if (processor_grayscale(_handle) != 0)
            throw new InvalidOperationException("Processor is null");
    }

    public void Dispose()
    {
        if (_handle != IntPtr.Zero)
        {
            processor_free(_handle);
            _handle = IntPtr.Zero;
        }
    }
}

// Использование — IDisposable гарантирует освобождение памяти Rust
using var proc = new ImageProcessor(1920, 1080);
proc.Grayscale();
// proc.Dispose() вызывается автоматически → processor_free() → Rust освобождает Vec

Ключевая мысль: это аналог паттерна SafeHandle в C#. Box::into_raw / Box::from_raw передают владение через границу FFI, а обёртка IDisposable в C# обеспечивает очистку.


Упражнения

🏋️ Упражнение: безопасная обёртка над сырым указателем (нажмите, чтобы раскрыть)

Вы получаете сырой указатель из C-библиотеки. Напишите безопасную обёртку на Rust:

#![allow(unused)]
fn main() {
// Имитация C API
extern "C" {
    fn lib_create_buffer(size: usize) -> *mut u8;
    fn lib_free_buffer(ptr: *mut u8);
}
}

Требования:

  1. Создайте структуру SafeBuffer, которая оборачивает сырой указатель
  2. Реализуйте Drop, который вызывает lib_free_buffer
  3. Предоставьте безопасное представление &[u8] через as_slice()
  4. Убедитесь, что SafeBuffer::new() возвращает None, если указатель null
🔑 Решение
struct SafeBuffer {
    ptr: *mut u8,
    len: usize,
}

impl SafeBuffer {
    fn new(size: usize) -> Option<Self> {
        // SAFETY: lib_create_buffer возвращает действительный указатель или null (проверяется ниже).
        let ptr = unsafe { lib_create_buffer(size) };
        if ptr.is_null() {
            None
        } else {
            Some(SafeBuffer { ptr, len: size })
        }
    }

    fn as_slice(&self) -> &[u8] {
        // SAFETY: ptr не null (проверено в new()), len — это выделенный размер,
        // и мы владеем буфером монопольно.
        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
    }
}

impl Drop for SafeBuffer {
    fn drop(&mut self) {
        // SAFETY: ptr был выделен через lib_create_buffer
        unsafe { lib_free_buffer(self.ptr); }
    }
}

// Использование: весь unsafe сосредоточен внутри SafeBuffer
fn process(buf: &SafeBuffer) {
    let data = buf.as_slice(); // полностью безопасный API
    println!("First byte: {}", data[0]);
}

Ключевой паттерн: инкапсулируйте unsafe в небольшом модуле с комментариями // SAFETY:. Открывайте наружу полностью безопасный публичный API. Так устроена стандартная библиотека Rust — Vec, String, HashMap внутри содержат unsafe-код, но предоставляют безопасные интерфейсы.


Тестирование

Тестирование в Rust и C#

Что вы узнаете: встроенный #[test] против xUnit, параметризованные тесты с rstest (аналог [Theory]), property-тестирование с proptest, моки с mockall и паттерны асинхронных тестов.

Сложность: 🟡 Средний

Модульные тесты

// C# — xUnit
using Xunit;

public class CalculatorTests
{
    [Fact]
    public void Add_ReturnsSum()
    {
        var calc = new Calculator();
        Assert.Equal(5, calc.Add(2, 3));
    }

    [Theory]
    [InlineData(1, 2, 3)]
    [InlineData(0, 0, 0)]
    [InlineData(-1, 1, 0)]
    public void Add_Theory(int a, int b, int expected)
    {
        Assert.Equal(expected, new Calculator().Add(a, b));
    }
}
#![allow(unused)]
fn main() {
// Rust — встроенное тестирование, внешний фреймворк не нужен
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]  // Компилируется только при `cargo test`
mod tests {
    use super::*;  // Импорт из родительского модуля

    #[test]
    fn add_returns_sum() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn add_negative_numbers() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn add_overflow_panics() {
        let _ = add(i32::MAX, 1); // паникует в debug-режиме
    }
}
}

Параметризованные тесты (аналог [Theory])

#![allow(unused)]
fn main() {
// Для параметризованных тестов используйте крейт `rstest`
use rstest::rstest;

#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[case(-1, 1, 0)]
fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
    assert_eq!(add(a, b), expected);
}

// Фикстуры — аналог методов настройки тестов
#[rstest]
fn test_with_fixture(#[values(1, 2, 3)] x: i32) {
    assert!(x > 0);
}
}

Сравнение утверждений

C# (xUnit)RustПримечания
Assert.Equal(expected, actual)assert_eq!(expected, actual)При провале выводит diff
Assert.NotEqual(a, b)assert_ne!(a, b)
Assert.True(condition)assert!(condition)
Assert.Contains("sub", str)assert!(str.contains("sub"))
Assert.Throws<T>(() => ...)#[should_panic]Или используйте std::panic::catch_unwind
Assert.Null(obj)assert!(option.is_none())Нет null — используйте Option

Организация тестов

my_crate/
├── src/
│   ├── lib.rs          # Модульные тесты в #[cfg(test)] mod tests { }
│   └── parser.rs       # У каждого модуля может быть свой тестовый модуль
├── tests/              # Интеграционные тесты (каждый файл — отдельный крейт)
│   ├── parser_test.rs  # Тестирует публичный API как внешний потребитель
│   └── api_test.rs
└── benches/            # Бенчмарки (с крейтом criterion)
    └── my_benchmark.rs
#![allow(unused)]
fn main() {
// tests/parser_test.rs — интеграционный тест
// Имеет доступ только к ПУБЛИЧНОМУ API (как тестирование извне сборки)
use my_crate::parser;

#[test]
fn test_parse_valid_input() {
    let result = parser::parse("valid input");
    assert!(result.is_ok());
}
}

Асинхронные тесты

// C# — асинхронный тест в xUnit
[Fact]
public async Task GetUser_ReturnsUser()
{
    var service = new UserService();
    var user = await service.GetUserAsync(1);
    Assert.Equal("Alice", user.Name);
}
#![allow(unused)]
fn main() {
// Rust — асинхронный тест с tokio
#[tokio::test]
async fn get_user_returns_user() {
    let service = UserService::new();
    let user = service.get_user(1).await.unwrap();
    assert_eq!(user.name, "Alice");
}
}

Моки с mockall

#![allow(unused)]
fn main() {
use mockall::automock;

#[automock]                         // Генерирует структуру MockUserRepo
trait UserRepo {
    fn find_by_id(&self, id: u32) -> Option<User>;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn service_returns_user_from_repo() {
        let mut mock = MockUserRepo::new();
        mock.expect_find_by_id()
            .with(mockall::predicate::eq(1))
            .returning(|_| Some(User { name: "Alice".into() }));

        let service = UserService::new(mock);
        let user = service.get_user(1).unwrap();
        assert_eq!(user.name, "Alice");
    }
}
}
// C# — эквивалент с Moq
var mock = new Mock<IUserRepo>();
mock.Setup(r => r.FindById(1)).Returns(new User { Name = "Alice" });
var service = new UserService(mock.Object);
Assert.Equal("Alice", service.GetUser(1).Name);
🏋️ Упражнение: напишите полноценные тесты (нажмите, чтобы раскрыть)

Задача: для этой функции напишите тесты, которые покрывают: основной сценарий, пустой ввод, числовые строки и Unicode.

#![allow(unused)]
fn main() {
pub fn title_case(input: &str) -> String {
    input.split_whitespace()
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(c) => format!("{}{}", c.to_uppercase(), chars.as_str().to_lowercase()),
                None => String::new(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}
}
🔑 Решение
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn happy_path() {
        assert_eq!(title_case("hello world"), "Hello World");
    }

    #[test]
    fn empty_input() {
        assert_eq!(title_case(""), "");
    }

    #[test]
    fn single_word() {
        assert_eq!(title_case("rust"), "Rust");
    }

    #[test]
    fn already_title_case() {
        assert_eq!(title_case("Hello World"), "Hello World");
    }

    #[test]
    fn all_caps() {
        assert_eq!(title_case("HELLO WORLD"), "Hello World");
    }

    #[test]
    fn extra_whitespace() {
        // split_whitespace обрабатывает несколько пробелов
        assert_eq!(title_case("  hello   world  "), "Hello World");
    }

    #[test]
    fn unicode() {
        assert_eq!(title_case("café résumé"), "Café Résumé");
    }

    #[test]
    fn numeric_words() {
        assert_eq!(title_case("hello 42 world"), "Hello 42 World");
    }
}
}

Ключевая мысль: встроенный тестовый фреймворк Rust покрывает большинство потребностей в модульном тестировании. Используйте rstest для параметризованных тестов и mockall для моков — большой тестовый фреймворк вроде xUnit не нужен.

Property-тестирование: доказательство корректности в масштабе

Разработчикам C#, знакомым с FsCheck, property-тестирование будет понятно сразу: вместо написания отдельных тест-кейсов вы описываете свойства, которые должны выполняться для всех возможных входных данных, а фреймворк генерирует тысячи случайных входов, чтобы их опровергнуть.

Зачем нужно property-тестирование

// C# — написанные вручную модульные тесты проверяют конкретные случаи
[Fact]
public void Reverse_Twice_Returns_Original()
{
    var list = new List<int> { 1, 2, 3 };
    list.Reverse();
    list.Reverse();
    Assert.Equal(new[] { 1, 2, 3 }, list);
}
// Но что насчёт пустых списков? Одиночных элементов? 10 000 элементов? Отрицательных чисел?
// Понадобились бы десятки тестов, написанных вручную.
#![allow(unused)]
fn main() {
// Rust — proptest автоматически генерирует тысячи входов
use proptest::prelude::*;

fn reverse<T: Clone>(v: &[T]) -> Vec<T> {
    v.iter().rev().cloned().collect()
}

proptest! {
    #[test]
    fn reverse_twice_is_identity(ref v in prop::collection::vec(any::<i32>(), 0..1000)) {
        let reversed_twice = reverse(&reverse(v));
        prop_assert_eq!(v, &reversed_twice);
    }
    // proptest запускает это на сотнях случайных значений Vec<i32>:
    // [], [0], [i32::MIN, i32::MAX], [42; 999], случайные последовательности...
    // Если тест падает, он СЖИМАЕТСЯ до наименьшего падающего входа!
}
}

Начало работы с proptest

# Cargo.toml
[dev-dependencies]
proptest = "1.4"

Распространённые паттерны для разработчиков C#

#![allow(unused)]
fn main() {
use proptest::prelude::*;

// 1. Свойство «туда и обратно»: сериализация → десериализация = тождество
// (Как тестирование JsonSerializer.Serialize → Deserialize)
proptest! {
    #[test]
    fn json_roundtrip(name in "[a-zA-Z]{1,50}", age in 0u32..150) {
        let user = User { name: name.clone(), age };
        let json = serde_json::to_string(&user).unwrap();
        let parsed: User = serde_json::from_str(&json).unwrap();
        prop_assert_eq!(user, parsed);
    }
}

// 2. Свойство-инвариант: результат всегда удовлетворяет условию
proptest! {
    #[test]
    fn sort_output_is_sorted(ref v in prop::collection::vec(any::<i32>(), 0..500)) {
        let mut sorted = v.clone();
        sorted.sort();
        // Каждая соседняя пара должна быть в порядке
        for window in sorted.windows(2) {
            prop_assert!(window[0] <= window[1]);
        }
    }
}

// 3. Свойство-оракул: сравниваем две реализации
proptest! {
    #[test]
    fn fast_path_matches_slow_path(input in "[0-9a-f]{1,100}") {
        let result_fast = parse_hex_fast(&input);
        let result_slow = parse_hex_slow(&input);
        prop_assert_eq!(result_fast, result_slow);
    }
}

// 4. Пользовательские стратегии: генерируем данные, специфичные для предметной области
fn valid_email() -> impl Strategy<Value = String> {
    ("[a-z]{1,20}", "[a-z]{1,10}", prop::sample::select(vec!["com", "org", "io"]))
        .prop_map(|(user, domain, tld)| format!("{}@{}.{}", user, domain, tld))
}

proptest! {
    #[test]
    fn email_parsing_accepts_valid_emails(email in valid_email()) {
        let result = Email::new(&email);
        prop_assert!(result.is_ok(), "Failed to parse: {}", email);
    }
}
}

Сравнение proptest и FsCheck

ВозможностьC# FsCheckRust proptest
Генерация случайных входовArb.Generate<T>()any::<T>()
Пользовательские генераторыArb.Register<T>()impl Strategy<Value = T>
Сжатие при падении (shrinking)АвтоматическоеАвтоматическое
Строковые шаблоныВручнуюСтратегия "[regex]"
Генерация коллекцийGen.ListOfprop::collection::vec(strategy, range)
Композиция генераторовGen.Select.prop_map(), .prop_flat_map()
Конфигурация (число случаев)Config.MaxTest#![proptest_config(ProptestConfig::with_cases(10000))] внутри блока proptest!

Когда использовать property-тестирование, а когда модульное

Используйте модульные тесты, когдаИспользуйте proptest, когда
Проверяются конкретные пограничные случаиПроверяются инварианты на всех входах
Проверяются сообщения об ошибках/кодыСвойства «туда и обратно» (разбор ↔ форматирование)
Интеграционные и тесты с мокамиСравнение двух реализаций
Поведение зависит от точных значений«Для всех X выполняется свойство P»

Интеграционные тесты: каталог tests/

Модульные тесты живут внутри src/ с #[cfg(test)]. Интеграционные тесты находятся в отдельном каталоге tests/ и проверяют публичный API крейта — так же, как интеграционные тесты в C# ссылаются на проект как на внешнюю сборку.

my_crate/
├── src/
│   ├── lib.rs          // публичный API
│   └── internal.rs     // приватная реализация
├── tests/
│   ├── smoke.rs        // каждый файл — отдельный тестовый бинарник
│   ├── api_tests.rs
│   └── common/
│       └── mod.rs      // общие вспомогательные функции для тестов
└── Cargo.toml

Написание интеграционных тестов

Каждый файл в tests/ компилируется как отдельный крейт, который зависит от вашей библиотеки:

#![allow(unused)]
fn main() {
// tests/smoke.rs — имеет доступ только к pub-элементам my_crate
use my_crate::{process_order, Order, OrderResult};

#[test]
fn process_valid_order_returns_confirmation() {
    let order = Order::new("SKU-001", 3);
    let result = process_order(order);
    assert!(matches!(result, OrderResult::Confirmed { .. }));
}
}

Общие вспомогательные функции для тестов

Общий код настройки кладите в tests/common/mod.rs (а не tests/common.rs, который будет считаться отдельным тестовым файлом):

#![allow(unused)]
fn main() {
// tests/common/mod.rs
use my_crate::Config;

pub fn test_config() -> Config {
    Config::builder()
        .database_url("sqlite::memory:")
        .build()
        .expect("test config must be valid")
}
}
#![allow(unused)]
fn main() {
// tests/api_tests.rs
mod common;

use my_crate::App;

#[test]
fn app_starts_with_test_config() {
    let config = common::test_config();
    let app = App::new(config);
    assert!(app.is_healthy());
}
}

Запуск отдельных типов тестов

cargo test                  # запустить все тесты (модульные + интеграционные)
cargo test --lib            # только модульные тесты (как dotnet test --filter Category=Unit)
cargo test --test smoke     # запустить только tests/smoke.rs
cargo test --test api_tests # запустить только tests/api_tests.rs

Ключевое отличие от C#: файлы интеграционных тестов видят только pub-API вашего крейта. Приватные функции невидимы — это заставляет тестировать через публичный интерфейс, что обычно даёт лучший дизайн тестов.


15. Паттерны миграции и разборы случаев

Распространённые паттерны C# в Rust

Что вы узнаете: как переводить с C# на идиоматичный Rust паттерн Repository, паттерн Builder, внедрение зависимостей, цепочки LINQ, запросы Entity Framework и паттерны конфигурации.

Сложность: 🟡 Средний

graph LR
    subgraph "Паттерн C#"
        I["interface IRepo&lt;T&gt;"] --> DI["Контейнер DI"]
        EX["try / catch"] --> LOG["ILogger"]
        LINQ["LINQ .Where().Select()"] --> LIST["List&lt;T&gt;"]
    end
    subgraph "Аналог в Rust"
        TR["trait Repo&lt;T&gt;"] --> GEN["Generic&lt;R: Repo&gt;"]
        RES["Result&lt;T, E&gt; + ?"] --> THISERR["thiserror / anyhow"]
        ITER[".iter().filter().map()"] --> VEC["Vec&lt;T&gt;"]
    end
    I -->|"становится"| TR
    EX -->|"становится"| RES
    LINQ -->|"становится"| ITER

    style TR fill:#c8e6c9,color:#000
    style RES fill:#c8e6c9,color:#000
    style ITER fill:#c8e6c9,color:#000

Паттерн Repository

// Паттерн Repository в C#
public interface IRepository<T> where T : IEntity
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

public class UserRepository : IRepository<User>
{
    private readonly DbContext _context;
    
    public UserRepository(DbContext context)
    {
        _context = context;
    }
    
    public async Task<User> GetByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }
    
    // ... остальные реализации
}
#![allow(unused)]
fn main() {
// Паттерн Repository в Rust с трейтами и обобщениями
use async_trait::async_trait;
use std::fmt::Debug;

#[async_trait]
pub trait Repository<T, E> 
where 
    T: Clone + Debug + Send + Sync,
    E: std::error::Error + Send + Sync,
{
    async fn get_by_id(&self, id: u64) -> Result<Option<T>, E>;
    async fn get_all(&self) -> Result<Vec<T>, E>;
    async fn add(&self, entity: T) -> Result<T, E>;
    async fn update(&self, entity: T) -> Result<T, E>;
    async fn delete(&self, id: u64) -> Result<(), E>;
}

#[derive(Debug, Clone)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub email: String,
}

#[derive(Debug)]
pub enum RepositoryError {
    NotFound(u64),
    DatabaseError(String),
    ValidationError(String),
}

impl std::fmt::Display for RepositoryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RepositoryError::NotFound(id) => write!(f, "Entity with id {} not found", id),
            RepositoryError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
            RepositoryError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
        }
    }
}

impl std::error::Error for RepositoryError {}

pub struct UserRepository {
    // пул соединений с базой данных и т. д.
}

#[async_trait]
impl Repository<User, RepositoryError> for UserRepository {
    async fn get_by_id(&self, id: u64) -> Result<Option<User>, RepositoryError> {
        // Имитация поиска в базе данных
        if id == 0 {
            return Ok(None);
        }
        
        Ok(Some(User {
            id,
            name: format!("User {}", id),
            email: format!("user{}@example.com", id),
        }))
    }
    
    async fn get_all(&self) -> Result<Vec<User>, RepositoryError> {
        // Реализация здесь
        Ok(vec![])
    }
    
    async fn add(&self, entity: User) -> Result<User, RepositoryError> {
        // Валидация и вставка в базу данных
        if entity.name.is_empty() {
            return Err(RepositoryError::ValidationError("Name cannot be empty".to_string()));
        }
        Ok(entity)
    }
    
    async fn update(&self, entity: User) -> Result<User, RepositoryError> {
        // Реализация здесь
        Ok(entity)
    }
    
    async fn delete(&self, id: u64) -> Result<(), RepositoryError> {
        // Реализация здесь
        Ok(())
    }
}
}

Паттерн Builder

// Паттерн Builder в C# (текучий интерфейс)
public class HttpClientBuilder
{
    private TimeSpan? _timeout;
    private string _baseAddress;
    private Dictionary<string, string> _headers = new();
    
    public HttpClientBuilder WithTimeout(TimeSpan timeout)
    {
        _timeout = timeout;
        return this;
    }
    
    public HttpClientBuilder WithBaseAddress(string baseAddress)
    {
        _baseAddress = baseAddress;
        return this;
    }
    
    public HttpClientBuilder WithHeader(string name, string value)
    {
        _headers[name] = value;
        return this;
    }
    
    public HttpClient Build()
    {
        var client = new HttpClient();
        if (_timeout.HasValue)
            client.Timeout = _timeout.Value;
        if (!string.IsNullOrEmpty(_baseAddress))
            client.BaseAddress = new Uri(_baseAddress);
        foreach (var header in _headers)
            client.DefaultRequestHeaders.Add(header.Key, header.Value);
        return client;
    }
}

// Использование
var client = new HttpClientBuilder()
    .WithTimeout(TimeSpan.FromSeconds(30))
    .WithBaseAddress("https://api.example.com")
    .WithHeader("Accept", "application/json")
    .Build();
#![allow(unused)]
fn main() {
// Паттерн Builder в Rust (потребляющий builder)
use std::collections::HashMap;
use std::time::Duration;

#[derive(Debug)]
pub struct HttpClient {
    timeout: Duration,
    base_address: String,
    headers: HashMap<String, String>,
}

pub struct HttpClientBuilder {
    timeout: Option<Duration>,
    base_address: Option<String>,
    headers: HashMap<String, String>,
}

impl HttpClientBuilder {
    pub fn new() -> Self {
        HttpClientBuilder {
            timeout: None,
            base_address: None,
            headers: HashMap::new(),
        }
    }
    
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }
    
    pub fn with_base_address<S: Into<String>>(mut self, base_address: S) -> Self {
        self.base_address = Some(base_address.into());
        self
    }
    
    pub fn with_header<K: Into<String>, V: Into<String>>(mut self, name: K, value: V) -> Self {
        self.headers.insert(name.into(), value.into());
        self
    }
    
    pub fn build(self) -> Result<HttpClient, String> {
        let base_address = self.base_address.ok_or("Base address is required")?;
        
        Ok(HttpClient {
            timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
            base_address,
            headers: self.headers,
        })
    }
}

// Использование
let client = HttpClientBuilder::new()
    .with_timeout(Duration::from_secs(30))
    .with_base_address("https://api.example.com")
    .with_header("Accept", "application/json")
    .build()?;

// Альтернатива: трейт Default для типичных случаев
impl Default for HttpClientBuilder {
    fn default() -> Self {
        Self::new()
    }
}
}

Сопоставление концепций C# и Rust

Внедрение зависимостей → внедрение через конструктор и трейты

// C# с контейнером DI
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserService, UserService>();

public class UserService
{
    private readonly IUserRepository _repository;
    
    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }
}
#![allow(unused)]
fn main() {
// Rust: внедрение через конструктор с трейтами
pub trait UserRepository {
    async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Error>;
    async fn save(&self, user: &User) -> Result<(), Error>;
}

pub struct UserService<R> 
where 
    R: UserRepository,
{
    repository: R,
}

impl<R> UserService<R> 
where 
    R: UserRepository,
{
    pub fn new(repository: R) -> Self {
        Self { repository }
    }
    
    pub async fn get_user(&self, id: Uuid) -> Result<Option<User>, Error> {
        self.repository.find_by_id(id).await
    }
}

// Использование
let repository = PostgresUserRepository::new(pool);
let service = UserService::new(repository);
}

LINQ → цепочки итераторов

// LINQ в C#
var result = users
    .Where(u => u.Age > 18)
    .Select(u => u.Name.ToUpper())
    .OrderBy(name => name)
    .Take(10)
    .ToList();
#![allow(unused)]
fn main() {
// Rust: цепочки итераторов (с нулевой стоимостью!)
let mut result: Vec<String> = users
    .iter()
    .filter(|u| u.age > 18)
    .map(|u| u.name.to_uppercase())
    .collect();
result.sort();
result.truncate(10);

// Или с крейтом itertools для более LINQ-подобной цепочки
use itertools::Itertools;

let result: Vec<String> = users
    .iter()
    .filter(|u| u.age > 18)
    .map(|u| u.name.to_uppercase())
    .sorted()
    .take(10)
    .collect();
}

Entity Framework → SQLx и миграции

// Entity Framework в C#
public class ApplicationDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
}

var user = await context.Users
    .Where(u => u.Email == email)
    .FirstOrDefaultAsync();
#![allow(unused)]
fn main() {
// Rust: SQLx с запросами, проверяемыми на этапе компиляции
use sqlx::{PgPool, FromRow};

#[derive(FromRow)]
struct User {
    id: Uuid,
    email: String,
    name: String,
}

// Запрос, проверяемый на этапе компиляции
let user = sqlx::query_as!(
    User,
    "SELECT id, email, name FROM users WHERE email = $1",
    email
)
.fetch_optional(&pool)
.await?;

// Или динамический запрос
let user = sqlx::query_as::<_, User>(
    "SELECT id, email, name FROM users WHERE email = $1"
)
.bind(email)
.fetch_optional(&pool)
.await?;
}

Конфигурация → крейты конфигурации

// Конфигурация в C#
public class AppSettings
{
    public string DatabaseUrl { get; set; }
    public int Port { get; set; }
}

var config = builder.Configuration.Get<AppSettings>();
#![allow(unused)]
fn main() {
// Rust: конфигурация через serde
use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct AppSettings {
    database_url: String,
    port: u16,
}

impl AppSettings {
    pub fn new() -> Result<Self, ConfigError> {
        let s = Config::builder()
            .add_source(File::with_name("config/default"))
            .add_source(Environment::with_prefix("APP"))
            .build()?;

        s.try_deserialize()
    }
}

// Использование
let settings = AppSettings::new()?;
}

Разборы случаев

Случай 1: миграция CLI-утилиты (csvtool)

Контекст: команда поддерживала консольное приложение на C# (CsvProcessor), которое читало большие CSV-файлы, применяло преобразования и записывало результат. При файлах в 500 МБ потребление памяти подскакивало до 4 ГБ, а паузы GC вызывали зависания на 30 секунд.

Подход к миграции: переписали на Rust за 2 недели, по одному модулю за раз.

ШагЧто изменилосьC# → Rust
1Разбор CSVCsvHelper → крейт csv (потоковый Reader)
2Модель данныхclass Record → struct Record (размещается на стеке, #[derive(Deserialize)])
3ПреобразованияLINQ .Select().Where() → .iter().map().filter()
4Файловый ввод-выводStreamReader → BufReader<File> с распространением ошибок через ?
5Аргументы CLISystem.CommandLine → clap с derive-макросами
6Параллельная обработкаParallel.ForEach → .par_iter() из rayon

Результаты:

  • Память: 4 ГБ → 12 МБ (потоковая обработка вместо загрузки всего файла)
  • Скорость: 45 с → 3 с для файла в 500 МБ
  • Размер бинарника: один исполняемый файл 2 МБ, без зависимости от рантайма

Главный урок: основной выигрыш был не в самом Rust — а в том, что модель владения Rust вынуждала к потоковому дизайну. В C# было легко сделать .ToList() и загрузить всё в память. В Rust проверщик заимствований естественно подталкивал к обработке на основе Iterator.

Случай 2: замена микросервиса (auth-gateway)

Контекст: шлюз аутентификации на C# ASP.NET Core отвечал за валидацию JWT и ограничение частоты запросов для более чем 50 бэкенд-сервисов. При 10 000 запросов в секунду p99-задержка достигала 200 мс из-за всплесков GC.

Подход к миграции: заменили на сервис на Rust с использованием axum + tower, сохранив тот же контракт API.

#![allow(unused)]
fn main() {
// Было (C#):  services.AddAuthentication().AddJwtBearer(...)
// Стало (Rust): слой middleware из tower

use axum::{Router, middleware};
use tower::ServiceBuilder;

let app = Router::new()
    .route("/api/*path", any(proxy_handler))
    .layer(
        ServiceBuilder::new()
            .layer(middleware::from_fn(validate_jwt))
            .layer(middleware::from_fn(rate_limit))
    );
}
МетрикаC# (ASP.NET Core)Rust (axum)
p50-задержка5 мс0,8 мс
p99-задержка200 мс (всплески GC)4 мс
Память300 МБ8 МБ
Docker-образ210 МБ (рантайм .NET)12 МБ (статический бинарник)
Холодный старт2,1 с0,05 с

Ключевые уроки:

  1. Сохраните тот же контракт API — изменения на стороне клиентов не нужны. Сервис на Rust стал прямой заменой.
  2. Начните с горячего пути — узким местом была валидация JWT. Миграция только этого middleware дала бы 80% выигрыша.
  3. Используйте middleware tower — он повторяет конвейер middleware ASP.NET Core, поэтому разработчикам C# архитектура на Rust показалась знакомой.
  4. Улучшение p99-задержки пришло от устранения пауз GC, а не от более быстрого кода — установившаяся пропускная способность Rust была лишь примерно в 2 раза выше, но отсутствие GC сделало «хвост» задержек предсказуемым.

Упражнения

🏋️ Упражнение: мигрируйте сервис на C# (нажмите, чтобы раскрыть)

Переведите этот сервис на C# на идиоматичный Rust:

public interface IUserService
{
    Task<User?> GetByIdAsync(int id);
    Task<List<User>> SearchAsync(string query);
}

public class UserService : IUserService
{
    private readonly IDatabase _db;
    public UserService(IDatabase db) { _db = db; }

    public async Task<User?> GetByIdAsync(int id)
    {
        try { return await _db.QuerySingleAsync<User>(id); }
        catch (NotFoundException) { return null; }
    }

    public async Task<List<User>> SearchAsync(string query)
    {
        return await _db.QueryAsync<User>($"SELECT * WHERE name LIKE '%{query}%'");
    }
}

Подсказки: используйте трейт, Option<User> вместо null, Result вместо try/catch, и устраните уязвимость SQL-инъекции.

🔑 Решение
#![allow(unused)]
fn main() {
use async_trait::async_trait;

#[derive(Debug, Clone)]
struct User { id: i64, name: String }

#[async_trait]
trait Database: Send + Sync {
    async fn get_user(&self, id: i64) -> Result<Option<User>, sqlx::Error>;
    async fn search_users(&self, query: &str) -> Result<Vec<User>, sqlx::Error>;
}

#[async_trait]
trait UserService: Send + Sync {
    async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError>;
    async fn search(&self, query: &str) -> Result<Vec<User>, AppError>;
}

struct UserServiceImpl<D: Database> {
    db: D,  // Arc не нужен — владение Rust справляется с этим само
}

#[async_trait]
impl<D: Database> UserService for UserServiceImpl<D> {
    async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError> {
        // Option вместо null; Result вместо try/catch
        Ok(self.db.get_user(id).await?)
    }

    async fn search(&self, query: &str) -> Result<Vec<User>, AppError> {
        // Параметризованный запрос — НИКАКИХ SQL-инъекций!
        // (sqlx использует плейсхолдеры $1, а не интерполяцию строк)
        self.db.search_users(query).await.map_err(Into::into)
    }
}
}

Ключевые изменения по сравнению с C#:

  • null → Option<User> (null-безопасность на этапе компиляции)
  • try/catch → Result + ? (явное распространение ошибок)
  • SQL-инъекция устранена: параметризованные запросы вместо интерполяции строк
  • IDatabase _db → обобщение D: Database (статическая диспетчеризация, без упаковки)

Основные крейты для разработчиков C#

Основные крейты для разработчиков C#

Что вы узнаете: аналоги крейтов Rust для распространённых библиотек .NET — serde (JSON.NET), reqwest (HttpClient), tokio (Task/async), sqlx (Entity Framework), а также подробный разбор системы атрибутов serde в сравнении с System.Text.Json.

Сложность: 🟡 Средний

Аналоги основной функциональности

#![allow(unused)]
fn main() {
// Зависимости Cargo для разработчиков C#
[dependencies]
Сериализация (как Newtonsoft.Json или System.Text.Json)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

HTTP-клиент (как HttpClient)
reqwest = { version = "0.11", features = ["json"] }

Асинхронный рантайм (как Task.Run, async/await)
tokio = { version = "1.0", features = ["full"] }

Обработка ошибок (как собственные исключения)
thiserror = "1.0"
anyhow = "1.0"

Логирование (как ILogger, Serilog)
log = "0.4"
env_logger = "0.10"

Дата и время (как DateTime)
chrono = { version = "0.4", features = ["serde"] }

UUID (как System.Guid)
uuid = { version = "1.0", features = ["v4", "serde"] }

Коллекции (как List<T>, Dictionary<K,V>)
Встроены в std, но для продвинутых коллекций:
indexmap = "2.0"  # HashMap с сохранением порядка

Конфигурация (как IConfiguration)
config = "0.13"

База данных (как Entity Framework)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }

Тестирование (как xUnit, NUnit)
Встроено в std, но для расширенных возможностей:
rstest = "0.18"  # Параметризованные тесты

Моки (как Moq)
mockall = "0.11"

Параллельная обработка (как Parallel.ForEach)
rayon = "1.7"
}

Примеры использования

use serde::{Deserialize, Serialize};
use reqwest;
use tokio;
use thiserror::Error;
use chrono::{DateTime, Utc};
use uuid::Uuid;

// Модели данных (как POCO с атрибутами в C#)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub created_at: DateTime<Utc>,
}

// Собственные типы ошибок (как собственные исключения)
#[derive(Error, Debug)]
pub enum ApiError {
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),
    
    #[error("Serialization failed: {0}")]
    Serialization(#[from] serde_json::Error),
    
    #[error("User not found: {id}")]
    UserNotFound { id: Uuid },
    
    #[error("Validation failed: {message}")]
    Validation { message: String },
}

// Аналог класса сервиса
pub struct UserService {
    client: reqwest::Client,
    base_url: String,
}

impl UserService {
    pub fn new(base_url: String) -> Self {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .expect("Failed to create HTTP client");
            
        UserService { client, base_url }
    }
    
    // Асинхронный метод (как async Task<User> в C#)
    pub async fn get_user(&self, id: Uuid) -> Result<User, ApiError> {
        let url = format!("{}/users/{}", self.base_url, id);
        
        let response = self.client
            .get(&url)
            .send()
            .await?;
        
        if response.status() == 404 {
            return Err(ApiError::UserNotFound { id });
        }
        
        let user = response.json::<User>().await?;
        Ok(user)
    }
    
    // Создание пользователя (как async Task<User> в C#)
    pub async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> {
        if name.trim().is_empty() {
            return Err(ApiError::Validation {
                message: "Name cannot be empty".to_string(),
            });
        }
        
        let new_user = User {
            id: Uuid::new_v4(),
            name,
            email,
            created_at: Utc::now(),
        };
        
        let response = self.client
            .post(&format!("{}/users", self.base_url))
            .json(&new_user)
            .send()
            .await?;
        
        let created_user = response.json::<User>().await?;
        Ok(created_user)
    }
}

// Пример использования (как метод Main в C#)
#[tokio::main]
async fn main() -> Result<(), ApiError> {
    // Инициализация логирования (как настройка ILogger)
    env_logger::init();
    
    let service = UserService::new("https://api.example.com".to_string());
    
    // Создание пользователя
    let user = service.create_user(
        "John Doe".to_string(),
        "john@example.com".to_string(),
    ).await?;
    
    println!("Created user: {:?}", user);
    
    // Получение пользователя
    let retrieved_user = service.get_user(user.id).await?;
    println!("Retrieved user: {:?}", retrieved_user);
    
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]  // Как [Test] или [Fact] в C#
    async fn test_user_creation() {
        let service = UserService::new("http://localhost:8080".to_string());
        
        let result = service.create_user(
            "Test User".to_string(),
            "test@example.com".to_string(),
        ).await;
        
        assert!(result.is_ok());
        let user = result.unwrap();
        assert_eq!(user.name, "Test User");
        assert_eq!(user.email, "test@example.com");
    }
    
    #[test]
    fn test_validation() {
        // Синхронный тест
        let error = ApiError::Validation {
            message: "Invalid input".to_string(),
        };
        
        assert_eq!(error.to_string(), "Validation failed: Invalid input");
    }
}

Глубокое погружение в serde: сериализация JSON для разработчиков C#

Разработчики C# активно используют System.Text.Json или Newtonsoft.Json. В Rust serde (serialize/deserialize) — универсальный фреймворк для этого. Понимание его системы атрибутов открывает большинство сценариев работы с данными.

Базовый derive: отправная точка

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    age: u32,
    email: String,
}

let user = User { name: "Alice".into(), age: 30, email: "alice@co.com".into() };
let json = serde_json::to_string_pretty(&user)?;
let parsed: User = serde_json::from_str(&json)?;
}
// Аналог в C#
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}
var json = JsonSerializer.Serialize(user, new JsonSerializerOptions { WriteIndented = true });
var parsed = JsonSerializer.Deserialize<User>(json);

Атрибуты уровня полей (аналог [JsonProperty])

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct ApiResponse {
    // Переименование поля в JSON (как [JsonPropertyName("user_id")])
    #[serde(rename = "user_id")]
    id: u64,

    // Разные имена при сериализации и десериализации
    #[serde(rename(serialize = "userName", deserialize = "user_name"))]
    name: String,

    // Полностью пропустить поле (как [JsonIgnore])
    #[serde(skip)]
    internal_cache: Option<String>,

    // Пропустить только при сериализации
    #[serde(skip_serializing)]
    password_hash: String,

    // Значение по умолчанию, если поля нет в JSON (как значения в конструкторе по умолчанию)
    #[serde(default)]
    is_active: bool,

    // Пользовательское значение по умолчанию
    #[serde(default = "default_role")]
    role: String,

    // Развернуть вложенную структуру в родительскую (как [JsonExtensionData])
    #[serde(flatten)]
    metadata: Metadata,

    // Пропустить, если значение None (не выводить null-поля)
    #[serde(skip_serializing_if = "Option::is_none")]
    nickname: Option<String>,
}

fn default_role() -> String { "viewer".into() }

#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
    created_at: String,
    version: u32,
}
}
// Эквивалентные атрибуты в C#
public class ApiResponse
{
    [JsonPropertyName("user_id")]
    public ulong Id { get; set; }

    [JsonIgnore]
    public string? InternalCache { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement>? Metadata { get; set; }
}

Представления перечислений (ключевое отличие от C#)

В serde для перечислений поддерживаются четыре разных представления в JSON — понятие, которому в C# нет прямого аналога, поскольку перечисления C# всегда целочисленные или строковые.

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

// 1. Внешне тегированное (ПО УМОЛЧАНИЮ) — самое распространённое
#[derive(Serialize, Deserialize)]
enum Message {
    Text(String),
    Image { url: String, width: u32 },
    Ping,
}
// Вариант Text:  {"Text": "hello"}
// Вариант Image: {"Image": {"url": "...", "width": 100}}
// Вариант Ping:  "Ping"

// 2. Внутренне тегированное — похоже на дискриминируемые объединения в других языках
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
    Created { id: u64, name: String },
    Deleted { id: u64 },
    Updated { id: u64, fields: Vec<String> },
}
// {"type": "Created", "id": 1, "name": "Alice"}
// {"type": "Deleted", "id": 1}

// 3. Смежно тегированное — тег и содержимое в отдельных полях
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum ApiResult {
    Success(UserData),
    Error(String),
}
// {"t": "Success", "c": {"name": "Alice"}}
// {"t": "Error", "c": "not found"}

// 4. Без тега — serde пробует варианты по порядку
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum FlexibleValue {
    Integer(i64),
    Float(f64),
    Text(String),
    Bool(bool),
}
// 42, 3.14, "hello", true — serde сам определяет вариант
}

Пользовательская сериализация (аналог JsonConverter)

#![allow(unused)]
fn main() {
use serde::{Deserialize, Deserializer, Serialize, Serializer};

// Пользовательская сериализация для конкретного поля
#[derive(Serialize, Deserialize)]
struct Config {
    #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")]
    timeout: std::time::Duration,
}

fn serialize_duration<S: Serializer>(dur: &std::time::Duration, s: S) -> Result<S::Ok, S::Error> {
    s.serialize_u64(dur.as_millis() as u64)
}

fn deserialize_duration<'de, D: Deserializer<'de>>(d: D) -> Result<std::time::Duration, D::Error> {
    let ms = u64::deserialize(d)?;
    Ok(std::time::Duration::from_millis(ms))
}
// JSON: {"timeout": 5000}  ↔  Config { timeout: Duration::from_millis(5000) }
}

Атрибуты уровня контейнера

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]  // Все поля в JSON становятся camelCase
struct UserProfile {
    first_name: String,      // → "firstName"
    last_name: String,       // → "lastName"
    email_address: String,   // → "emailAddress"
}

#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]  // Отвергать JSON с лишними полями (строгий разбор)
struct StrictConfig {
    port: u16,
    host: String,
}
// serde_json::from_str::<StrictConfig>(r#"{"port":8080,"host":"localhost","extra":true}"#)
// → Error: unknown field `extra`
}

Краткая справка по атрибутам serde

АтрибутУровеньАналог в C#Назначение
#[serde(rename = "...")]Поле[JsonPropertyName]Переименование в JSON
#[serde(skip)]Поле[JsonIgnore]Полностью пропустить
#[serde(default)]ПолеЗначение по умолчаниюИспользовать Default::default(), если поля нет
#[serde(flatten)]Поле[JsonExtensionData]Слить вложенную структуру в родительскую
#[serde(skip_serializing_if = "...")]ПолеJsonIgnoreConditionУсловный пропуск
#[serde(rename_all = "camelCase")]КонтейнерJsonSerializerOptions.PropertyNamingPolicyСоглашение об именовании
#[serde(deny_unknown_fields)]Контейнер—Строгая десериализация
#[serde(tag = "type")]ПеречислениеПаттерн дискриминатораВнутреннее тегирование
#[serde(untagged)]Перечисление—Пробовать варианты по порядку
#[serde(with = "...")]Поле[JsonConverter]Пользовательская сериализация и десериализация

За пределами JSON: serde работает везде

#![allow(unused)]
fn main() {
// ТОТ ЖЕ derive работает для ВСЕХ форматов — нужно лишь сменить крейт
let user = User { name: "Alice".into(), age: 30, email: "a@b.com".into() };

let json  = serde_json::to_string(&user)?;        // JSON
let toml  = toml::to_string(&user)?;               // TOML (конфигурационные файлы)
let yaml  = serde_yaml::to_string(&user)?;          // YAML
let cbor  = serde_cbor::to_vec(&user)?;             // CBOR (бинарный, компактный)
let msgpk = rmp_serde::to_vec(&user)?;              // MessagePack (бинарный)

// Один #[derive(Serialize, Deserialize)] — каждый формат бесплатно
}

Стратегия постепенного внедрения

Стратегия постепенного внедрения

Что вы узнаете: поэтапный подход к внедрению Rust в организации, работающие на C#/.NET — от учебных упражнений (недели 1–4) через замену компонентов, критичных к производительности (недели 5–8), до новых микросервисов (недели 9–12), с конкретными сроками освоения для команды.

Сложность: 🟡 Средний

Этап 1: обучение и эксперименты (недели 1–4)

// Начинаем с инструментов командной строки и утилит
// Пример: анализатор файлов логов
use std::fs;
use std::collections::HashMap;
use clap::Parser;

#[derive(Parser)]
#[command(author, version, about)]
struct Args {
    #[arg(short, long)]
    file: String,
    
    #[arg(short, long, default_value = "10")]
    top: usize,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    
    let content = fs::read_to_string(&args.file)?;
    let mut word_count = HashMap::new();
    
    for line in content.lines() {
        for word in line.split_whitespace() {
            let word = word.to_lowercase();
            *word_count.entry(word).or_insert(0) += 1;
        }
    }
    
    let mut sorted: Vec<_> = word_count.into_iter().collect();
    sorted.sort_by(|a, b| b.1.cmp(&a.1));
    
    for (word, count) in sorted.into_iter().take(args.top) {
        println!("{}: {}", word, count);
    }
    
    Ok(())
}

Этап 2: замена компонентов, критичных к производительности (недели 5–8)

// Замена ресурсоёмкой обработки данных
// Пример: микросервис обработки изображений
use image::{DynamicImage, ImageBuffer, Rgb};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use warp::Filter;

#[derive(Serialize, Deserialize)]
struct ProcessingRequest {
    image_data: Vec<u8>,
    operation: String,
    parameters: serde_json::Value,
}

#[derive(Serialize)]
struct ProcessingResponse {
    processed_image: Vec<u8>,
    processing_time_ms: u64,
}

async fn process_image(request: ProcessingRequest) -> Result<ProcessingResponse, Box<dyn std::error::Error + Send + Sync>> {
    let start = std::time::Instant::now();
    
    let img = image::load_from_memory(&request.image_data)?;
    
    let processed = match request.operation.as_str() {
        "blur" => {
            let radius = request.parameters["radius"].as_f64().unwrap_or(2.0) as f32;
            img.blur(radius)
        }
        "grayscale" => img.grayscale(),
        "resize" => {
            let width = request.parameters["width"].as_u64().unwrap_or(100) as u32;
            let height = request.parameters["height"].as_u64().unwrap_or(100) as u32;
            img.resize(width, height, image::imageops::FilterType::Lanczos3)
        }
        _ => return Err("Unknown operation".into()),
    };
    
    let mut buffer = Vec::new();
    processed.write_to(&mut std::io::Cursor::new(&mut buffer), image::ImageOutputFormat::Png)?;
    
    Ok(ProcessingResponse {
        processed_image: buffer,
        processing_time_ms: start.elapsed().as_millis() as u64,
    })
}

#[tokio::main]
async fn main() {
    let process_route = warp::path("process")
        .and(warp::post())
        .and(warp::body::json())
        .and_then(|req: ProcessingRequest| async move {
            match process_image(req).await {
                Ok(response) => Ok(warp::reply::json(&response)),
                Err(e) => Err(warp::reject::custom(ProcessingError(e.to_string()))),
            }
        });

    warp::serve(process_route)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

#[derive(Debug)]
struct ProcessingError(String);
impl warp::reject::Reject for ProcessingError {}

Этап 3: новые микросервисы (недели 9–12)

// Строим новые сервисы с нуля на Rust
// Пример: сервис аутентификации
use axum::{
    extract::{Query, State},
    http::StatusCode,
    response::Json,
    routing::{get, post},
    Router,
};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres};
use uuid::Uuid;
use bcrypt::{hash, verify, DEFAULT_COST};

#[derive(Clone)]
struct AppState {
    db: Pool<Postgres>,
    jwt_secret: String,
}

#[derive(Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
}

#[derive(Deserialize)]
struct LoginRequest {
    email: String,
    password: String,
}

#[derive(Serialize)]
struct LoginResponse {
    token: String,
    user_id: Uuid,
}

async fn login(
    State(state): State<AppState>,
    Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
    // Примечание: sqlx::query!() проверяется на этапе компиляции и требует, чтобы DATABASE_URL
    // указывал на работающую базу данных во время сборки. Для запросов, проверяемых во время выполнения,
    // используйте sqlx::query() или sqlx::query_as().
    let user = sqlx::query!(
        "SELECT id, password_hash FROM users WHERE email = $1",
        request.email
    )
    .fetch_optional(&state.db)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let user = user.ok_or(StatusCode::UNAUTHORIZED)?;

    if !verify(&request.password, &user.password_hash)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
    {
        return Err(StatusCode::UNAUTHORIZED);
    }

    let claims = Claims {
        sub: user.id.to_string(),
        exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(state.jwt_secret.as_ref()),
    )
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(Json(LoginResponse {
        token,
        user_id: user.id,
    }))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let database_url = std::env::var("DATABASE_URL")?;
    let jwt_secret = std::env::var("JWT_SECRET")?;
    
    let pool = sqlx::postgres::PgPoolOptions::new()
        .max_connections(20)
        .connect(&database_url)
        .await?;

    let app_state = AppState {
        db: pool,
        jwt_secret,
    };

    let app = Router::new()
        .route("/login", post(login))
        .with_state(app_state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    
    Ok(())
}

Календарь освоения для команды

Месяц 1: основы

Недели 1–2: синтаксис и владение

  • Базовые синтаксические отличия от C#
  • Понимание владения, заимствования и времён жизни
  • Небольшие упражнения: CLI-утилиты, обработка файлов

Недели 3–4: обработка ошибок и типы

  • Result<T, E> против исключений
  • Option<T> против nullable-типов
  • Сопоставление с образцом и проверка исчерпываемости

Рекомендуемые упражнения:

#![allow(unused)]
fn main() {
// Недели 1–2: обработчик файлов
fn process_log_file(path: &str) -> Result<Vec<String>, std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    let errors: Vec<String> = content
        .lines()
        .filter(|line| line.contains("ERROR"))
        .map(|line| line.to_string())
        .collect();
    Ok(errors)
}

// Недели 3–4: обработчик JSON с обработкой ошибок
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
struct LogEntry {
    timestamp: String,
    level: String,
    message: String,
}

fn parse_log_entries(json_str: &str) -> Result<Vec<LogEntry>, Box<dyn std::error::Error>> {
    let entries: Vec<LogEntry> = serde_json::from_str(json_str)?;
    Ok(entries)
}
}

Месяц 2: практическое применение

Недели 5–6: трейты и обобщения

  • Система трейтов против интерфейсов
  • Ограничения обобщений и границы
  • Распространённые паттерны и идиомы

Недели 7–8: асинхронное программирование и конкурентность

  • Сходства и различия async/await
  • Каналы для коммуникации
  • Гарантии потокобезопасности

Рекомендуемые проекты:

#![allow(unused)]
fn main() {
// Недели 5–6: обобщённый обработчик данных
trait DataProcessor<T> {
    type Output;
    type Error;
    
    fn process(&self, data: T) -> Result<Self::Output, Self::Error>;
}

struct JsonProcessor;

impl DataProcessor<&str> for JsonProcessor {
    type Output = serde_json::Value;
    type Error = serde_json::Error;
    
    fn process(&self, data: &str) -> Result<Self::Output, Self::Error> {
        serde_json::from_str(data)
    }
}

// Недели 7–8: асинхронный веб-клиент
async fn fetch_and_process_data(urls: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    
    let tasks: Vec<_> = urls
        .into_iter()
        .map(|url| {
            let client = client.clone();
            tokio::spawn(async move {
                let response = client.get(url).send().await?;
                let text = response.text().await?;
                println!("Fetched {} bytes from {}", text.len(), url);
                Ok::<(), reqwest::Error>(())
            })
        })
        .collect();
    
    for task in tasks {
        task.await??;
    }
    
    Ok(())
}
}

Месяц 3 и далее: интеграция в продакшен

Недели 9–12: работа над реальным проектом

  • Выберите некритичный компонент для переписывания
  • Реализуйте полноценную обработку ошибок
  • Добавьте логирование, метрики и тестирование
  • Профилирование производительности и оптимизация

Постоянно: командный ревью и менторство

  • Код-ревью с фокусом на идиомах Rust
  • Парное программирование
  • Сессии обмена знаниями

16. Лучшие практики

Лучшие практики для разработчиков C#

Что вы узнаете: пять ключевых сдвигов мышления (GC→владение, исключения→Result, наследование→композиция), идиоматичную организацию проекта, стратегию обработки ошибок, паттерны тестирования и самые частые ошибки разработчиков C# в Rust.

Сложность: 🟡 Средний

1. Сдвиги мышления

  • От GC к владению: думайте о том, кому принадлежат данные и когда их освобождают
  • От исключений к Result: делайте обработку ошибок явной и видимой
  • От наследования к композиции: используйте трейты для композиции поведения
  • От null к Option: делайте отсутствие значения явным в системе типов

2. Организация кода

#![allow(unused)]
fn main() {
// Структурируем проекты, как решения (solutions) C#
src/
├── main.rs          // Аналог Program.cs
├── lib.rs           // Точка входа библиотеки
├── models/          // Аналог папки Models/ в C#
│   ├── mod.rs
│   ├── user.rs
│   └── product.rs
├── services/        // Аналог папки Services/
│   ├── mod.rs
│   ├── user_service.rs
│   └── product_service.rs
├── controllers/     // Аналог Controllers/ (для веб-приложений)
├── repositories/    // Аналог Repositories/
└── utils/           // Аналог Utilities/
}

3. Стратегия обработки ошибок

#![allow(unused)]
fn main() {
// Создаём общий тип Result для приложения
pub type AppResult<T> = Result<T, AppError>;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),
    
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),
    
    #[error("Validation error: {message}")]
    Validation { message: String },
    
    #[error("Business logic error: {message}")]
    Business { message: String },
}

// Используем во всём приложении
pub async fn create_user(data: CreateUserRequest) -> AppResult<User> {
    validate_user_data(&data)?;  // Возвращает AppError::Validation
    let user = repository.create_user(data).await?;  // Возвращает AppError::Database
    Ok(user)
}
}

4. Паттерны тестирования

#![allow(unused)]
fn main() {
// Структурируем тесты, как модульные тесты C#
#[cfg(test)]
mod tests {
    use super::*;
    use rstest::*;  // Для параметризованных тестов, как [Theory] в C#
    
    #[test]
    fn test_basic_functionality() {
        // Arrange
        let input = "test data";
        
        // Act
        let result = process_data(input);
        
        // Assert
        assert_eq!(result, "expected output");
    }
    
    #[rstest]
    #[case(1, 2, 3)]
    #[case(5, 5, 10)]
    #[case(0, 0, 0)]
    fn test_addition(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
        assert_eq!(add(a, b), expected);
    }
    
    #[tokio::test]  // Для асинхронных тестов
    async fn test_async_functionality() {
        let result = async_function().await;
        assert!(result.is_ok());
    }
}
}

5. Частые ошибки, которых стоит избегать

#![allow(unused)]
fn main() {
// [ERROR] Не пытайтесь реализовать наследование
// Вместо:
// struct Manager : Employee  // В Rust такого нет

// [OK] Используйте композицию через трейты
trait Employee {
    fn get_salary(&self) -> u32;
}

trait Manager: Employee {
    fn get_team_size(&self) -> usize;
}

// [ERROR] Не используйте unwrap() везде (это как игнорирование исключений)
let value = might_fail().unwrap();  // Может вызвать панику!

// [OK] Обрабатывайте ошибки правильно
let value = match might_fail() {
    Ok(v) => v,
    Err(e) => {
        log::error!("Operation failed: {}", e);
        return Err(e.into());
    }
};

// [ERROR] Не клонируйте всё подряд (как лишнее копирование объектов)
let data = expensive_data.clone();  // Дорого!

// [OK] Используйте заимствование, когда возможно
let data = &expensive_data;  // Всего лишь ссылка

// [ERROR] Не используйте RefCell повсюду (как превращение всего в изменяемое)
struct Data {
    value: RefCell<i32>,  // Внутренняя изменяемость — используйте осторожно
}

// [OK] Предпочитайте владеемые или заимствованные данные
struct Data {
    value: i32,  // Просто и понятно
}
}

Это руководство даёт разработчикам C# цельное представление о том, как их существующие знания переносятся на Rust, подчёркивая как сходства, так и принципиальные различия в подходе. Ключевое — понимать, что ограничения Rust (например, владение) призваны предотвратить целые классы ошибок, возможных в C#, ценой некоторой первоначальной сложности.


6. Избегаем избыточного clone() 🟡

Разработчики C# инстинктивно клонируют данные, потому что стоимость берёт на себя GC. В Rust каждый .clone() — это явное выделение памяти. Большинство из них можно убрать с помощью заимствования.

#![allow(unused)]
fn main() {
// [ERROR] Привычка из C#: клонировать строки, чтобы передавать их дальше
fn greet(name: String) {
    println!("Hello, {name}");
}

let user_name = String::from("Alice");
greet(user_name.clone());  // ненужное выделение памяти
greet(user_name.clone());  // и снова

// [OK] Заимствуйте вместо этого — без выделений памяти
fn greet(name: &str) {
    println!("Hello, {name}");
}

let user_name = String::from("Alice");
greet(&user_name);  // заимствуем
greet(&user_name);  // заимствуем снова — без затрат
}

Когда clone уместен:

  • Перемещение данных в поток или в замыкание 'static (Arc::clone дёшев — он лишь увеличивает счётчик)
  • Кэширование: вам действительно нужна независимая копия
  • Прототипирование: сначала заставьте работать, потом уберите лишние clone

Чек-лист решения:

  1. Можно передать &T или &str? → Так и сделайте
  2. Вызываемой стороне нужно владение? → Передавайте перемещением, а не клоном
  3. Данные разделяются между потоками? → Используйте Arc<T> (clone — это лишь увеличение счётчика ссылок)
  4. Ничего из перечисленного? → clone() оправдан

7. Избегаем unwrap() в продакшен-коде 🟡

Разработчики C#, которые игнорируют исключения, пишут .unwrap() везде в Rust. И то и другое одинаково опасно.

#![allow(unused)]
fn main() {
// [ERROR] Ловушка «потом поправлю»
let config = std::fs::read_to_string("config.toml").unwrap();
let port: u16 = config_value.parse().unwrap();
let conn = db_pool.get().await.unwrap();

// [OK] Пробрасываем через ? в прикладном коде
let config = std::fs::read_to_string("config.toml")?;
let port: u16 = config_value.parse()?;
let conn = db_pool.get().await?;

// [OK] Используйте expect() только тогда, когда сбой — действительно ошибка в программе
let home = std::env::var("HOME")
    .expect("HOME environment variable must be set");  // фиксирует инвариант
}

Эмпирическое правило:

МетодКогда использовать
?Код приложения и библиотек — передать вызывающему коду
expect("reason")Проверки при запуске, инварианты, которые обязаны выполняться
unwrap()Только в тестах или после проверки is_some()/is_ok()
unwrap_or(default)Когда есть разумное значение по умолчанию
`unwrap_or_else(

8. Борьба с проверщиком заимствований (и как перестать бороться) 🟡

Каждый разработчик C# проходит этап, когда проверщик заимствований отвергает код, который кажется правильным. Исправление обычно требует изменения структуры, а не обходного манёвра.

#![allow(unused)]
fn main() {
// [ERROR] Попытка изменять коллекцию во время перебора (паттерн foreach + изменение из C#)
let mut items = vec![1, 2, 3, 4, 5];
for item in &items {
    if *item > 3 {
        items.push(*item * 2);  // ОШИБКА: нельзя заимствовать items как изменяемое
    }
}

// [OK] Сначала собираем, потом изменяем
let extras: Vec<i32> = items.iter()
    .filter(|&&x| x > 3)
    .map(|&x| x * 2)
    .collect();
items.extend(extras);
}
#![allow(unused)]
fn main() {
// [ERROR] Возврат ссылки на локальную переменную (в C# ссылки свободно возвращаются благодаря GC)
fn get_greeting() -> &str {
    let s = String::from("hello");
    &s  // ОШИБКА: s уничтожается в конце функции
}

// [OK] Возвращаем владеемые данные
fn get_greeting() -> String {
    String::from("hello")  // вызывающий код владеет ими
}
}

Распространённые паттерны, которые снимают конфликты с проверщиком заимствований:

Привычка из C#Решение в Rust
Хранить ссылки в структурахИспользовать владеемые данные или добавить параметры времён жизни
Свободно изменять разделяемое состояниеИспользовать Arc<Mutex<T>> или перестроить код, чтобы не разделять данные
Возвращать ссылки на локальные переменныеВозвращать владеемые значения
Изменять коллекцию во время перебораСначала собрать изменения, потом применить
Несколько изменяемых ссылокРазбить структуру на независимые части

9. Устраняем «пирамиды» присваиваний 🟢

Разработчики C# пишут цепочки вида if (x != null) { if (x.Value > 0) { ... } }. Конструкции match, if let и ? в Rust позволяют выровнять такой код.

#![allow(unused)]
fn main() {
// [ERROR] Вложенный стиль проверок на null из C#
fn process(input: Option<String>) -> Option<usize> {
    match input {
        Some(s) => {
            if !s.is_empty() {
                match s.parse::<usize>() {
                    Ok(n) => {
                        if n > 0 {
                            Some(n * 2)
                        } else {
                            None
                        }
                    }
                    Err(_) => None,
                }
            } else {
                None
            }
        }
        None => None,
    }
}

// [OK] Выравниваем с помощью комбинаторов
fn process(input: Option<String>) -> Option<usize> {
    input
        .filter(|s| !s.is_empty())
        .and_then(|s| s.parse::<usize>().ok())
        .filter(|&n| n > 0)
        .map(|n| n * 2)
}
}

Ключевые комбинаторы, которые должен знать каждый разработчик C#:

КомбинаторЧто делаетАналог в C#
mapПреобразует внутреннее значениеSelect / null-условный ?.
and_thenСвязывает операции, возвращающие Option/ResultSelectMany / ?.Method()
filterОставляет значение, только если предикат истиненWhere
unwrap_orЗадаёт значение по умолчанию?? defaultValue
ok()Превращает Result в Option (отбрасывая ошибку)—
transposeМеняет местами Option<Result> и Result<Option>—

Сравнение производительности и миграция

Сравнение производительности: управляемый и нативный код

Что вы узнаете: реальные различия в производительности между C# и Rust — время запуска, потребление памяти, бенчмарки пропускной способности, задачи, интенсивные по CPU, а также дерево решений о том, когда стоит мигрировать, а когда остаться на C#.

Сложность: 🟡 Средний

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

АспектC# (.NET)RustВлияние на производительность
Время запуска100–500 мс (JIT); 5–30 мс (.NET 8 AOT)1–10 мс (нативный бинарник)🚀 В 10–50 раз быстрее (относительно JIT)
Потребление памяти+30–100% (накладные расходы GC и метаданных)Базовый уровень (минимальный рантайм)💾 На 30–50% меньше RAM
Паузы GCПериодические паузы 1–100 мсНикогда (нет GC)⚡ Стабильная задержка
Загрузка CPU+10–20% (накладные расходы GC и JIT)Базовый уровень (прямое исполнение)🔋 На 10–20% эффективнее
Размер бинарника30–200 МБ (с рантаймом); 10–30 МБ (AOT с обрезкой)1–20 МБ (статический бинарник)📦 Компактнее развёртывание
Безопасность памятиПроверки во время выполненияДоказательства на этапе компиляции🛡️ Безопасность без накладных расходов
Производительность при конкурентностиХорошая (при аккуратной синхронизации)Отличная (конкурентность без страха)🏃 Превосходная масштабируемость

Примечание о .NET 8+ AOT: нативная AOT-компиляция существенно сокращает разрыв во времени запуска (5–30 мс). По пропускной способности и памяти накладные расходы GC и паузы сохраняются. Оценивая миграцию, измеряйте производительность на вашей конкретной нагрузке — цифры из заголовков могут вводить в заблуждение.

Примеры бенчмарков

// C# — бенчмарк обработки JSON
public class JsonProcessor
{
    public async Task<List<User>> ProcessJsonFile(string path)
    {
        var json = await File.ReadAllTextAsync(path);
        var users = JsonSerializer.Deserialize<List<User>>(json);
        
        return users.Where(u => u.Age > 18)
                   .OrderBy(u => u.Name)
                   .Take(1000)
                   .ToList();
    }
}

// Типичная производительность: ~200 мс для файла 100 МБ
// Пиковое потребление памяти: ~500 МБ (накладные расходы GC)
// Размер бинарника: ~80 МБ (самодостаточный)
#![allow(unused)]
fn main() {
// Rust — эквивалентная обработка JSON
use serde::{Deserialize, Serialize};
use tokio::fs;

#[derive(Deserialize, Serialize)]
struct User {
    name: String,
    age: u32,
}

pub async fn process_json_file(path: &str) -> Result<Vec<User>, Box<dyn std::error::Error>> {
    let json = fs::read_to_string(path).await?;
    let mut users: Vec<User> = serde_json::from_str(&json)?;
    
    users.retain(|u| u.age > 18);
    users.sort_by(|a, b| a.name.cmp(&b.name));
    users.truncate(1000);
    
    Ok(users)
}

// Типичная производительность: ~120 мс для того же файла 100 МБ
// Пиковое потребление памяти: ~200 МБ (без накладных расходов GC)
// Размер бинарника: ~8 МБ (статический бинарник)
}

Задачи, интенсивные по CPU

// C# — математические вычисления
public class Mandelbrot
{
    public static int[,] Generate(int width, int height, int maxIterations)
    {
        var result = new int[height, width];
        
        Parallel.For(0, height, y =>
        {
            for (int x = 0; x < width; x++)
            {
                var c = new Complex(
                    (x - width / 2.0) * 4.0 / width,
                    (y - height / 2.0) * 4.0 / height);
                
                result[y, x] = CalculateIterations(c, maxIterations);
            }
        });
        
        return result;
    }
}

// Производительность: ~2,3 секунды (машина с 8 ядрами)
// Память: ~500 МБ
#![allow(unused)]
fn main() {
// Rust — то же вычисление с Rayon
use rayon::prelude::*;
use num_complex::Complex;

pub fn generate_mandelbrot(width: usize, height: usize, max_iterations: u32) -> Vec<Vec<u32>> {
    (0..height)
        .into_par_iter()
        .map(|y| {
            (0..width)
                .map(|x| {
                    let c = Complex::new(
                        (x as f64 - width as f64 / 2.0) * 4.0 / width as f64,
                        (y as f64 - height as f64 / 2.0) * 4.0 / height as f64,
                    );
                    calculate_iterations(c, max_iterations)
                })
                .collect()
        })
        .collect()
}

// Производительность: ~1,1 секунды (та же машина с 8 ядрами)
// Память: ~200 МБ
// В 2 раза быстрее при на 60% меньшем потреблении памяти
}

Когда выбирать каждый язык

Выбирайте C#, когда:

  • Быстрая разработка критична — богатая экосистема инструментов
  • Экспертиза команды в .NET — имеющиеся знания и навыки
  • Интеграция с корпоративными системами — активное использование экосистемы Microsoft
  • Умеренные требования к производительности — производительности достаточно
  • Приложения с богатым UI — WPF, WinUI, Blazor
  • Прототипы и MVP — быстрый выход на рынок

Выбирайте Rust, когда:

  • Производительность критична — приложения, интенсивные по CPU и памяти
  • Важны ограничения ресурсов — встраиваемые системы, edge-вычисления, serverless
  • Долгоживущие сервисы — веб-серверы, базы данных, системные сервисы
  • Системное программирование — компоненты ОС, драйверы, сетевые утилиты
  • Высокие требования к надёжности — финансовые системы, системы критической безопасности
  • Конкурентные и параллельные нагрузки — обработка данных с высокой пропускной способностью

Дерево решений о стратегии миграции

graph TD
    START["Рассматриваете Rust?"]
    PERFORMANCE["Производительность критична?"]
    TEAM["У команды есть время на обучение?"]
    EXISTING["Большая существующая кодовая база на C#?"]
    NEW_PROJECT["Новый проект или компонент?"]
    
    INCREMENTAL["Постепенное внедрение:<br/>• Сначала CLI-инструменты<br/>• Компоненты, критичные к производительности<br/>• Новые микросервисы"]
    
    FULL_RUST["Полный переход на Rust:<br/>• Новые проекты с нуля<br/>• Системные сервисы<br/>• Высокопроизводительные API"]
    
    STAY_CSHARP["Остаться на C#:<br/>• Оптимизировать существующий код<br/>• Использовать возможности .NET AOT / производительности<br/>• Рассмотреть .NET Native"]
    
    START --> PERFORMANCE
    PERFORMANCE -->|Да| TEAM
    PERFORMANCE -->|Нет| STAY_CSHARP
    
    TEAM -->|Да| EXISTING
    TEAM -->|Нет| STAY_CSHARP
    
    EXISTING -->|Да| NEW_PROJECT
    EXISTING -->|Нет| FULL_RUST
    
    NEW_PROJECT -->|Новый| FULL_RUST
    NEW_PROJECT -->|Существующий| INCREMENTAL
    
    style FULL_RUST fill:#c8e6c9,color:#000
    style INCREMENTAL fill:#fff3e0,color:#000
    style STAY_CSHARP fill:#e3f2fd,color:#000

Путь обучения и ресурсы

Путь обучения и следующие шаги

Что вы узнаете: структурированную дорожную карту обучения (недели 1–2, месяцы 1–3+), рекомендуемые книги и ресурсы, типичные ловушки для разработчиков C# (путаница с владением, борьба с проверщиком заимствований), а также структурированную наблюдаемость с tracing против ILogger.

Сложность: 🟢 Начальный

Ближайшие шаги (недели 1–2)

  1. Настройте окружение

    • Установите Rust через rustup.rs
    • Настройте VS Code с расширением rust-analyzer
    • Создайте первый проект cargo new hello_world
  2. Освойте основы

    • Практикуйте владение на простых упражнениях
    • Пишите функции с разными типами параметров (&str, String, &mut)
    • Реализуйте простые структуры и методы
  3. Практика обработки ошибок

    • Переведите код C# с try-catch на паттерны на основе Result
    • Практикуйтесь с оператором ? и выражениями match
    • Реализуйте собственные типы ошибок

Промежуточные цели (месяцы 1–2)

  1. Коллекции и итераторы

    • Освойте Vec<T>, HashMap<K,V> и HashSet<T>
    • Изучите методы итераторов: map, filter, collect, fold
    • Практикуйтесь: циклы for против цепочек итераторов
  2. Трейты и обобщения

    • Реализуйте распространённые трейты: Debug, Clone, PartialEq
    • Пишите обобщённые функции и структуры
    • Разберитесь с границами трейтов и предложениями where
  3. Структура проекта

    • Организуйте код в модули
    • Разберитесь с видимостью pub
    • Работайте с внешними крейтами с crates.io

Продвинутые темы (месяцы 3 и далее)

  1. Конкурентность

    • Изучите трейты Send и Sync
    • Используйте std::thread для базового параллелизма
    • Изучите tokio для асинхронного программирования
  2. Управление памятью

    • Разберитесь с Rc<T> и Arc<T> для разделяемого владения
    • Изучите, когда использовать Box<T> для выделения в куче
    • Освойте времена жизни для сложных сценариев
  3. Реальные проекты

    • Напишите CLI-утилиту с clap
    • Создайте веб-API с axum или warp
    • Напишите библиотеку и опубликуйте её на crates.io

Рекомендуемые ресурсы для обучения

Книги

  • «The Rust Programming Language» (бесплатно онлайн) — официальная книга
  • «Rust by Example» (бесплатно онлайн) — примеры для практики
  • «Programming Rust» Джима Блэнди — глубокое техническое изложение

Онлайн-ресурсы

Практические проекты

  1. Калькулятор командной строки — практика с перечислениями и сопоставлением с образцом
  2. Организатор файлов — работа с файловой системой и обработкой ошибок
  3. Обработчик JSON — изучение serde и преобразования данных
  4. HTTP-сервер — понимание асинхронного программирования и сетей
  5. Библиотека для работы с базой данных — освоение трейтов, обобщений и обработки ошибок

Типичные ловушки для разработчиков C#

Путаница с владением

#![allow(unused)]
fn main() {
// НЕ НАДО: пытаться использовать перемещённые значения
fn wrong_way() {
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s); // ОШИБКА: s было перемещено
}

// НАДО: используйте ссылки или клонируйте при необходимости
fn right_way() {
    let s = String::from("hello");
    borrows_string(&s);
    println!("{}", s); // OK: s здесь по-прежнему принадлежит нам
}

fn takes_ownership(s: String) { /* s перемещается сюда */ }
fn borrows_string(s: &str) { /* s заимствуется сюда */ }
}

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

#![allow(unused)]
fn main() {
// НЕ НАДО: несколько изменяемых ссылок одновременно
fn wrong_borrowing() {
    let mut v = vec![1, 2, 3];
    let r1 = &mut v;
    // let r2 = &mut v; // ОШИБКА: нельзя заимствовать как изменяемое больше одного раза
}

// НАДО: ограничивайте область видимости изменяемых заимствований
fn right_borrowing() {
    let mut v = vec![1, 2, 3];
    {
        let r1 = &mut v;
        r1.push(4);
    } // r1 выходит из области видимости здесь
    
    let r2 = &mut v; // OK: других изменяемых заимствований нет
    r2.push(5);
}
}

Ожидание значений null

#![allow(unused)]
fn main() {
// НЕ НАДО: ожидать поведения, похожего на null
fn no_null_in_rust() {
    // let s: String = null; // В Rust НЕТ null!
}

// НАДО: явно используйте Option<T>
fn use_option_instead() {
    let maybe_string: Option<String> = None;
    
    match maybe_string {
        Some(s) => println!("Got string: {}", s),
        None => println!("No string available"),
    }
}
}

Финальные советы

  1. Прислушивайтесь к компилятору — ошибки компилятора Rust полезны, а не враждебны
  2. Начинайте с малого — начните с простых программ и постепенно усложняйте
  3. Читайте чужой код — изучайте популярные крейты на GitHub
  4. Просите помощи — сообщество Rust дружелюбно и отзывчиво
  5. Практикуйтесь регулярно — концепции Rust становятся естественными с практикой

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


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

Структурированная наблюдаемость: tracing против ILogger и Serilog

Разработчики C# привыкли к структурированному логированию через ILogger, Serilog или NLog — где сообщения журнала несут типизированные пары ключ-значение. Крейт log в Rust даёт базовое логирование с уровнями, но tracing — производственный стандарт структурированной наблюдаемости со спанами, поддержкой асинхронности и распределённой трассировкой.

Почему tracing, а не log

ВозможностьКрейт logКрейт tracingАналог в C#
Сообщения с уровнями✅ info!(), error!()✅ info!(), error!()ILogger.LogInformation()
Структурированные поля❌ Только интерполяция строк✅ Типизированные пары ключ-значениеSerilog Log.Information("{User}", user)
Спаны (ограниченный контекст)❌✅ #[instrument], span!()ILogger.BeginScope()
Поддержка асинхронности❌ Контекст теряется на .await✅ Спаны сохраняются через .awaitActivity / DiagnosticSource
Распределённая трассировка❌✅ Интеграция с OpenTelemetrySystem.Diagnostics.Activity
Несколько форматов выводаБазовыйJSON, pretty, compact, OTLPПриёмники (sinks) Serilog

Начало работы

# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

Базовое использование: структурированное логирование

// Serilog в C#
Log.Information("Processing order {OrderId} for {Customer}, total {Total:C}",
    orderId, customer.Name, order.Total);
// Вывод: Processing order 12345 for Alice, total $99.95
// JSON:  {"OrderId": 12345, "Customer": "Alice", "Total": 99.95, ...}
#![allow(unused)]
fn main() {
use tracing::{info, warn, error, debug, instrument};

// Структурированные поля — типизированные, а не интерполированные в строку
info!(order_id = 12345, customer = "Alice", total = 99.95,
      "Processing order");
// Вывод: INFO Processing order order_id=12345 customer="Alice" total=99.95
// JSON:  {"order_id": 12345, "customer": "Alice", "total": 99.95, ...}

// Динамические значения
let order_id = 12345;
info!(order_id, "Order received");  // сокращение: имя поля = имя переменной

// Условные поля
if let Some(promo) = promo_code {
    info!(order_id, promo_code = %promo, "Promo applied");
    //                        ^ % означает использовать форматирование Display
    //                        ? использовал бы форматирование Debug
}
}

Спаны: ключевая возможность для асинхронного кода

Спаны — это ограниченные контексты, которые переносят поля через вызовы функций и точки .await. Это как ILogger.BeginScope(), но безопасно для асинхронного кода.

// C# — Activity / BeginScope
using var activity = new Activity("ProcessOrder").Start();
activity.SetTag("order_id", orderId);

using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId }))
{
    _logger.LogInformation("Starting processing");
    await ProcessPaymentAsync();
    _logger.LogInformation("Payment complete");  // OrderId всё ещё в области видимости
}
#![allow(unused)]
fn main() {
use tracing::{info, instrument, Instrument};

// #[instrument] автоматически создаёт спан с аргументами функции в качестве полей
#[instrument(skip(db), fields(customer_name))]
async fn process_order(order_id: u64, db: &Database) -> Result<(), AppError> {
    let order = db.get_order(order_id).await?;
    
    // Динамически добавляем поле в текущий спан
    tracing::Span::current().record("customer_name", &order.customer_name.as_str());
    
    info!("Starting processing");
    process_payment(&order).await?;        // контекст спана сохраняется через .await!
    info!(items = order.items.len(), "Payment complete");
    Ok(())
}
// Каждое сообщение журнала внутри этой функции автоматически содержит:
//   order_id=12345 customer_name="Alice"
// Даже во вложенных асинхронных вызовах!

// Ручное создание спана (как BeginScope)
async fn batch_process(orders: Vec<u64>, db: &Database) {
    for order_id in orders {
        let span = tracing::info_span!("process_order", order_id);
        
        // .instrument(span) прикрепляет спан к future
        process_order(order_id, db)
            .instrument(span)
            .await
            .unwrap_or_else(|e| error!("Failed: {e}"));
    }
}
}

Настройка подписчика (аналог приёмников Serilog)

#![allow(unused)]
fn main() {
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};

fn init_tracing() {
    // Разработка: читаемый человеком цветной вывод
    tracing_subscriber::registry()
        .with(EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| "my_app=debug,tower_http=info".into()))
        .with(fmt::layer().pretty())  // Цветные, с отступами спаны
        .init();
}

fn init_tracing_production() {
    // Продакшен: вывод в JSON для агрегации логов (как JSON-приёмник Serilog)
    tracing_subscriber::registry()
        .with(EnvFilter::new("my_app=info"))
        .with(fmt::layer().json())  // Структурированный JSON
        .init();
    // Вывод: {"timestamp":"...","level":"INFO","fields":{"order_id":123},...}
}
}
# Управление уровнями логирования через переменную окружения (как MinimumLevel в Serilog)
RUST_LOG=my_app=debug,hyper=warn cargo run
RUST_LOG=trace cargo run  # всё подряд

Шпаргалка по переходу с Serilog на tracing

Serilog / ILoggertracingПримечания
Log.Information("{Key}", val)info!(key = val, "message")Поля типизированы, не интерполируются
Log.ForContext("Key", val)span.record("key", val)Добавляет поля в текущий спан
using BeginScope(...)#[instrument] или info_span!()Автоматически с #[instrument]
.WriteTo.Console()fmt::layer()Читаемый человеком вывод
.WriteTo.Seq() / .File()fmt::layer().json() + перенаправление в файлИли используйте tracing-appender
.Enrich.WithProperty()span!(Level::INFO, "name", key = val)Поля спана
LogEventLevel.Debugtracing::Level::DEBUGТа же концепция
Деструктуризация {@Object}field = ?value (Debug) или %value (Display)? = Debug, % = Display

Интеграция с OpenTelemetry

# Для распределённой трассировки (как System.Diagnostics + экспортёр OTLP)
[dependencies]
tracing-opentelemetry = "0.22"
opentelemetry = "0.21"
opentelemetry-otlp = "0.14"
#![allow(unused)]
fn main() {
// Добавляем слой OpenTelemetry вместе с выводом в консоль
use tracing_opentelemetry::OpenTelemetryLayer;

fn init_otel() {
    let tracer = opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(opentelemetry_otlp::new_exporter().tonic())
        .install_batch(opentelemetry_sdk::runtime::Tokio)
        .expect("Failed to create OTLP tracer");

    tracing_subscriber::registry()
        .with(OpenTelemetryLayer::new(tracer))  // Отправляем спаны в Jaeger/Tempo
        .with(fmt::layer())                      // Также выводим в консоль
        .init();
}
// Теперь спаны от #[instrument] автоматически становятся распределёнными трассами!
}

Экосистема инструментов Rust

Основные инструменты Rust для разработчиков C#

Что вы узнаете: инструменты разработки Rust и их аналоги в C# — Clippy (анализаторы Roslyn), rustfmt (dotnet format), cargo doc (XML-документация), cargo watch (dotnet watch) и расширения VS Code.

Сложность: 🟢 Начальный

Сравнение инструментов

Инструмент C#Аналог в RustУстановкаНазначение
Анализаторы RoslynClippyrustup component add clippyЛинтинг и подсказки по стилю
dotnet formatrustfmtrustup component add rustfmtАвтоматическое форматирование
XML-комментарии к документацииcargo docВстроенГенерация HTML-документации
OmniSharp / Roslynrust-analyzerРасширение VS CodeПоддержка в IDE
dotnet watchcargo-watchcargo install cargo-watchАвтоматическая пересборка при сохранении
—cargo-expandcargo install cargo-expandПросмотр раскрытия макросов
dotnet auditcargo-auditcargo install cargo-auditПроверка уязвимостей безопасности

Clippy: ваш автоматический код-ревьюер

# Запуск Clippy для проекта
cargo clippy

# Считать предупреждения ошибками (CI/CD)
cargo clippy -- -D warnings

# Автоматическое применение подсказок
cargo clippy --fix
#![allow(unused)]
fn main() {
// Clippy находит сотни антипаттернов:

// До Clippy:
if x == true { }           // предупреждение: проверка равенства с bool
let _ = vec.len() == 0;    // предупреждение: используйте .is_empty()
for i in 0..vec.len() { }  // предупреждение: используйте .iter().enumerate()

// После применения подсказок Clippy:
if x { }
let _ = vec.is_empty();
for (i, item) in vec.iter().enumerate() { }
}

rustfmt: единообразное форматирование

# Форматирование всех файлов
cargo fmt

# Проверка форматирования без изменений (CI/CD)
cargo fmt -- --check
# rustfmt.toml — настройка форматирования (как .editorconfig)
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true

cargo doc: генерация документации

# Генерация и открытие документации (включая зависимости)
cargo doc --open

# Запуск тестов документации
cargo test --doc
#![allow(unused)]
fn main() {
/// Вычисляет площадь круга.
///
/// # Аргументы
/// * `radius` — радиус круга (должен быть неотрицательным)
///
/// # Примеры
/// ```
/// let area = my_crate::circle_area(5.0);
/// assert!((area - 78.54).abs() < 0.01);
/// ```
///
/// # Паника
/// Паникует, если `radius` отрицательный.
pub fn circle_area(radius: f64) -> f64 {
    assert!(radius >= 0.0, "radius must be non-negative");
    std::f64::consts::PI * radius * radius
}
// Код внутри блоков /// ``` компилируется и запускается во время `cargo test`!
}

cargo watch: автоматическая пересборка

# Пересборка при изменении файлов (как dotnet watch)
cargo watch -x check          # Только проверка типов (самый быстрый)
cargo watch -x test           # Запуск тестов при сохранении
cargo watch -x 'run -- args'  # Запуск программы при сохранении
cargo watch -x clippy         # Линтинг при сохранении

cargo expand: просмотр того, что генерируют макросы

# Просмотр раскрытого вывода производных макросов
cargo expand --lib            # Раскрыть lib.rs
cargo expand module_name      # Раскрыть конкретный модуль

Рекомендуемые расширения VS Code

РасширениеНазначение
rust-analyzerАвтодополнение кода, встроенные ошибки, рефакторинг
CodeLLDBОтладчик (как отладчик Visual Studio)
Even Better TOMLПодсветка синтаксиса Cargo.toml
cratesПоказ последних версий крейтов в Cargo.toml
Error LensВывод ошибок и предупреждений прямо в строке

Для более глубокого изучения продвинутых тем, упомянутых в этом руководстве, см. сопутствующие учебные материалы:

  • Паттерны Rust — проекции Pin, пользовательские аллокаторы, арена-паттерны, структуры данных без блокировок и продвинутые паттерны unsafe
  • Async Rust Training — подробно о tokio, безопасности отмены в асинхронном коде, обработке потоков и производственной архитектуре async
  • Rust для программистов C/C++ — полезно, если в команде есть опыт работы с C++; рассматривает соответствие семантики перемещения, различия RAII и шаблоны против обобщений
  • Rust для программистов C — актуально для сценариев взаимодействия; рассматривает паттерны FFI, отладку встраиваемого Rust и программирование с no_std

17. Итоговый проект: CLI-утилита для погоды на Rust

Итоговый проект: CLI-утилита для погоды на Rust

Что вы узнаете: как объединить всё изученное — структуры, трейты, обработку ошибок, асинхронность, модули, serde и разбор аргументов командной строки — в работающее приложение на Rust. Это повторяет тот тип утилиты, который разработчик C# написал бы с HttpClient, System.Text.Json и System.CommandLine.

Сложность: 🟡 Средний

Этот итоговый проект собирает концепции из всех частей книги. Вы создадите weather-cli — утилиту командной строки, которая получает данные о погоде из API и выводит их. Проект организован как мини-крейт с правильной структурой модулей, типами ошибок и тестами.

Обзор проекта

graph TD
    CLI["main.rs<br/>разбор CLI через clap"] --> Client["client.rs<br/>reqwest + tokio"]
    Client -->|"HTTP GET"| API["Погодный API"]
    Client -->|"JSON → структура"| Model["weather.rs<br/>serde Deserialize"]
    Model --> Display["display.rs<br/>fmt::Display"]
    CLI --> Err["error.rs<br/>thiserror"]
    Client --> Err

    style CLI fill:#bbdefb,color:#000
    style Err fill:#ffcdd2,color:#000
    style Model fill:#c8e6c9,color:#000

Что вы создадите:

$ weather-cli --city "Seattle"
🌧  Seattle: 12°C, Overcast clouds
    Влажность: 82%  Ветер: 5.4 м/с

Используемые концепции:

Глава книгиКонцепция, используемая здесь
Гл. 05 (Структуры)Типы данных WeatherReport, Config
Гл. 08 (Модули)src/lib.rs, src/client.rs, src/display.rs
Гл. 09 (Ошибки)Собственный WeatherError с thiserror
Гл. 10 (Трейты)Реализация Display для форматированного вывода
Гл. 11 (From/Into)Десериализация JSON через serde
Гл. 12 (Итераторы)Обработка массивов из ответа API
Гл. 13 (Async)reqwest + tokio для HTTP-запросов
Гл. 14-1 (Тестирование)Модульные тесты и интеграционный тест

Шаг 1: настройка проекта

cargo new weather-cli
cd weather-cli

Добавьте зависимости в Cargo.toml:

[package]
name = "weather-cli"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }   # Аргументы CLI (как System.CommandLine)
reqwest = { version = "0.12", features = ["json"] } # HTTP-клиент (как HttpClient)
serde = { version = "1", features = ["derive"] }    # Сериализация (как System.Text.Json)
serde_json = "1"
thiserror = "2"                                      # Типы ошибок
tokio = { version = "1", features = ["full"] }       # Асинхронный рантайм
// Эквивалентные зависимости в C#:
// dotnet add package System.CommandLine
// dotnet add package System.Net.Http.Json
// (System.Text.Json и HttpClient входят в состав платформы)

Шаг 2: определите типы данных

Создайте src/weather.rs:

#![allow(unused)]
fn main() {
use serde::Deserialize;

/// Сырой ответ API (повторяет структуру JSON)
#[derive(Deserialize, Debug)]
pub struct ApiResponse {
    pub main: MainData,
    pub weather: Vec<WeatherCondition>,
    pub wind: WindData,
    pub name: String,
}

#[derive(Deserialize, Debug)]
pub struct MainData {
    pub temp: f64,
    pub humidity: u32,
}

#[derive(Deserialize, Debug)]
pub struct WeatherCondition {
    pub description: String,
    pub icon: String,
}

#[derive(Deserialize, Debug)]
pub struct WindData {
    pub speed: f64,
}

/// Наш доменный тип (чистый, отделённый от API)
#[derive(Debug, Clone)]
pub struct WeatherReport {
    pub city: String,
    pub temp_celsius: f64,
    pub description: String,
    pub humidity: u32,
    pub wind_speed: f64,
}

impl From<ApiResponse> for WeatherReport {
    fn from(api: ApiResponse) -> Self {
        let description = api.weather
            .first()
            .map(|w| w.description.clone())
            .unwrap_or_else(|| "Unknown".to_string());

        WeatherReport {
            city: api.name,
            temp_celsius: api.main.temp,
            description,
            humidity: api.main.humidity,
            wind_speed: api.wind.speed,
        }
    }
}
}
// Эквивалент в C#:
// public record ApiResponse(MainData Main, List<WeatherCondition> Weather, ...);
// public record WeatherReport(string City, double TempCelsius, ...);
// Ручное маппирование или AutoMapper

Ключевое отличие: #[derive(Deserialize)] + реализация From заменяют JsonSerializer.Deserialize<T>() + AutoMapper. В Rust оба этих шага выполняются на этапе компиляции — без рефлексии.

Шаг 3: тип ошибок

Создайте src/error.rs:

#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Error, Debug)]
pub enum WeatherError {
    #[error("Ошибка HTTP-запроса: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Город не найден: {0}")]
    CityNotFound(String),

    #[error("Ключ API не задан — выполните export WEATHER_API_KEY")]
    MissingApiKey,
}

pub type Result<T> = std::result::Result<T, WeatherError>;
}

Шаг 4: HTTP-клиент

Создайте src/client.rs:

#![allow(unused)]
fn main() {
use crate::error::{WeatherError, Result};
use crate::weather::{ApiResponse, WeatherReport};

pub struct WeatherClient {
    api_key: String,
    http: reqwest::Client,
}

impl WeatherClient {
    pub fn new(api_key: String) -> Self {
        WeatherClient {
            api_key,
            http: reqwest::Client::new(),
        }
    }

    pub async fn get_weather(&self, city: &str) -> Result<WeatherReport> {
        let url = format!(
            "https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric",
            city, self.api_key
        );

        let response = self.http.get(&url).send().await?;

        if response.status() == reqwest::StatusCode::NOT_FOUND {
            return Err(WeatherError::CityNotFound(city.to_string()));
        }

        let api_data: ApiResponse = response.json().await?;
        Ok(WeatherReport::from(api_data))
    }
}
}
// Эквивалент в C#:
// var response = await _httpClient.GetAsync(url);
// if (response.StatusCode == HttpStatusCode.NotFound)
//     throw new CityNotFoundException(city);
// var data = await response.Content.ReadFromJsonAsync<ApiResponse>();

Ключевые отличия:

  • Оператор ? заменяет try/catch — ошибки распространяются автоматически через Result
  • WeatherReport::from(api_data) использует трейт From вместо AutoMapper
  • IHttpClientFactory не нужен — reqwest::Client сам управляет пулом соединений

Шаг 5: форматирование вывода

Создайте src/display.rs:

#![allow(unused)]
fn main() {
use std::fmt;
use crate::weather::WeatherReport;

impl fmt::Display for WeatherReport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let icon = weather_icon(&self.description);
        writeln!(f, "{}  {}: {:.0}°C, {}",
            icon, self.city, self.temp_celsius, self.description)?;
        write!(f, "    Влажность: {}%  Ветер: {:.1} м/с",
            self.humidity, self.wind_speed)
    }
}

fn weather_icon(description: &str) -> &str {
    let desc = description.to_lowercase();
    if desc.contains("clear") { "☀️" }
    else if desc.contains("cloud") { "☁️" }
    else if desc.contains("rain") || desc.contains("drizzle") { "🌧" }
    else if desc.contains("snow") { "❄️" }
    else if desc.contains("thunder") { "⛈" }
    else { "🌡" }
}
}

Шаг 6: собираем всё вместе

src/lib.rs:

#![allow(unused)]
fn main() {
pub mod client;
pub mod display;
pub mod error;
pub mod weather;
}

src/main.rs:

use clap::Parser;
use weather_cli::{client::WeatherClient, error::WeatherError};

#[derive(Parser)]
#[command(name = "weather-cli", about = "Получение погоды из командной строки")]
struct Cli {
    /// Название города для поиска
    #[arg(short, long)]
    city: String,
}

#[tokio::main]
async fn main() {
    let cli = Cli::parse();

    let api_key = match std::env::var("WEATHER_API_KEY") {
        Ok(key) => key,
        Err(_) => {
            eprintln!("Ошибка: {}", WeatherError::MissingApiKey);
            std::process::exit(1);
        }
    };

    let client = WeatherClient::new(api_key);

    match client.get_weather(&cli.city).await {
        Ok(report) => println!("{report}"),
        Err(WeatherError::CityNotFound(city)) => {
            eprintln!("Город не найден: {city}");
            std::process::exit(1);
        }
        Err(e) => {
            eprintln!("Ошибка: {e}");
            std::process::exit(1);
        }
    }
}

Шаг 7: тесты

#![allow(unused)]
fn main() {
// В src/weather.rs или tests/weather_test.rs
#[cfg(test)]
mod tests {
    use super::*;

    fn sample_api_response() -> ApiResponse {
        serde_json::from_str(r#"{
            "main": {"temp": 12.3, "humidity": 82},
            "weather": [{"description": "overcast clouds", "icon": "04d"}],
            "wind": {"speed": 5.4},
            "name": "Seattle"
        }"#).unwrap()
    }

    #[test]
    fn api_response_to_weather_report() {
        let report = WeatherReport::from(sample_api_response());
        assert_eq!(report.city, "Seattle");
        assert!((report.temp_celsius - 12.3).abs() < 0.01);
        assert_eq!(report.description, "overcast clouds");
    }

    #[test]
    fn display_format_includes_icon() {
        let report = WeatherReport {
            city: "Test".into(),
            temp_celsius: 20.0,
            description: "clear sky".into(),
            humidity: 50,
            wind_speed: 3.0,
        };
        let output = format!("{report}");
        assert!(output.contains("☀️"));
        assert!(output.contains("20°C"));
    }

    #[test]
    fn empty_weather_array_defaults_to_unknown() {
        let json = r#"{
            "main": {"temp": 0.0, "humidity": 0},
            "weather": [],
            "wind": {"speed": 0.0},
            "name": "Nowhere"
        }"#;
        let api: ApiResponse = serde_json::from_str(json).unwrap();
        let report = WeatherReport::from(api);
        assert_eq!(report.description, "Unknown");
    }
}
}

Итоговая структура файлов

weather-cli/
├── Cargo.toml
├── src/
│   ├── main.rs        # Точка входа CLI (clap)
│   ├── lib.rs         # Объявления модулей
│   ├── client.rs      # HTTP-клиент (reqwest + tokio)
│   ├── weather.rs     # Типы данных + реализация From + тесты
│   ├── display.rs     # Форматирование вывода
│   └── error.rs       # WeatherError + псевдоним Result
└── tests/
    └── integration.rs # Интеграционные тесты

Сравните с эквивалентом на C#:

WeatherCli/
├── WeatherCli.csproj
├── Program.cs
├── Services/
│   └── WeatherClient.cs
├── Models/
│   ├── ApiResponse.cs
│   └── WeatherReport.cs
└── Tests/
    └── WeatherTests.cs

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

  • Объявления mod вместо пространств имён
  • Result<T, E> вместо исключений
  • Трейт From вместо AutoMapper
  • Явный #[tokio::main] вместо встроенного асинхронного рантайма

Бонус: заготовка интеграционного теста

Создайте tests/integration.rs, чтобы проверить публичный API без обращения к настоящему серверу:

#![allow(unused)]
fn main() {
// tests/integration.rs
use weather_cli::weather::WeatherReport;

#[test]
fn weather_report_display_roundtrip() {
    let report = WeatherReport {
        city: "Seattle".into(),
        temp_celsius: 12.3,
        description: "overcast clouds".into(),
        humidity: 82,
        wind_speed: 5.4,
    };

    let output = format!("{report}");
    assert!(output.contains("Seattle"));
    assert!(output.contains("12°C"));
    assert!(output.contains("82%"));
}
}

Запустите cargo test — Rust автоматически находит тесты и в src/ (модули #[cfg(test)]), и в tests/ (интеграционные тесты). Никакой настройки тестового фреймворка не требуется — сравните с настройкой xUnit/NUnit в C#.


Задания для продолжения

Когда всё заработает, попробуйте эти задания, чтобы углубить навыки:

  1. Добавьте кэширование — сохраняйте последний ответ API в файл. При запуске проверяйте, не старше ли он 10 минут, и если нет — пропускайте HTTP-запрос. Это упражнение на std::fs, serde_json::to_writer и SystemTime.

  2. Добавьте поддержку нескольких городов — принимайте --city "Seattle,Portland,Vancouver" и загружайте все города конкурентно через tokio::join!. Это упражнение на конкурентное асинхронное программирование.

  3. Добавьте флаг --format json — выводите отчёт в формате JSON вместо читаемого текста с помощью serde_json::to_string_pretty. Это упражнение на условное форматирование и Serialize.

  4. Напишите интеграционный тест — создайте tests/integration.rs, который проверяет полный сценарий с имитацией HTTP-сервера через wiremock. Это упражнение на шаблон каталога tests/ из главы 14-1.