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 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