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 нужны макросы (нет перегрузки и вариативных аргументов), основы 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, только для структуры кода, а не значений.