Макросы — это функция метапрограммирования Rust, которая позволяет генерировать код во время компиляции. Макросы позволяют абстрагироваться и манипулировать синтаксическими деревьями во время компиляции. Это мощный способ расширить язык в соответствии с вашими потребностями.

Макросы имеют следующие основные применения:

  • Генерация повторяющегося кода. Макросы можно использовать для краткой генерации шаблонного кода или повторяющейся логики. Это позволяет избежать дублирования и делает код СУХИМ (не повторяйтесь).
  • Обработка шаблонной логики. Макросы могут аккуратно инкапсулировать и абстрагировать шаблонную логику. Это делает код чище и проще для понимания.
  • Реализация шаблонов проектирования. Макросы можно использовать для краткой реализации определенных шаблонов проектирования, которые в противном случае потребовали бы большого количества шаблонного кода. Например, шаблон Observer можно реализовать с помощью макросов.
  • Расширьте язык: макросы позволяют создавать собственные конструкции, которые расширяются до более сложной логики. Это позволяет вам эффективно расширять язык Rust в соответствии с вашим доменом или вариантом использования.

Макросы имеют ряд преимуществ перед функциями:

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

Подводя итог, макросы позволяют вам абстрагировать код Rust и манипулировать им таким образом, что расширяет возможности языка. Они обеспечивают мощное метапрограммирование и создание индивидуальных расширений синтаксиса. Макросы — ключевая часть раскрытия всего потенциала Rust как гибкого и расширяемого языка.

Синтаксис макросов

Макросы определяются с помощью конструкции macro_rules!. Основной синтаксис:

macro_rules! name {
    /* macro definition */
}

Чтобы вызвать макрос, используйте префикс !:

name!(/* macro arguments */);

Существует несколько типов макроаргументов:

  • $ident: Соответствует идентификатору (имени переменной).
  • $t:ty: Соответствует типу
  • $(...)*: соответствует 0 или более чем-либо.
  • $(...),+: соответствует одному или нескольким значениям, разделенным запятыми.

Например, вот базовый макрос, который дублирует свой аргумент:

macro_rules! double {
    ($x:expr) => {
        $x * 2
    };
}

fn main() {
    let a = double!(2); // a = 4
}

У нас также могут быть макросы с несколькими аргументами:

macro_rules! sum {
    ($a:expr, $b:expr, $c:expr) => {
        $a + $b + $c 
    };
}

fn main() {
    let total = sum!(1, 2, 3); // total = 6
}

Макросы также могут расширяться на несколько операторов:

macro_rules! unless {
    ($cond:expr) => {
        if !$cond {
            /* body */
        }
    };
}

unless!(false) {
    println!("Condition was false");
}

Мы можем использовать $(...)* для повторения элементов:

macro_rules! repeat {
    ($x:expr; $($y:expr),+) => {
        $x $($y)*
    };
}

fn main() {
    let a = repeat!(1; 2, 3); // a = 1 2 3 1 2 3 
}

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

III. Встроенные макросы

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

распечататьлн!

Макрос println! выводит на консоль строку, за которой следует перевод строки:

println!("Hello World!");

Он также может печатать несколько аргументов разных типов:

println!("My name is {} and I'm {} years old", "John", 30);

очень хорошо!

Макрос vec! создает новый вектор с заданными элементами:

let v = vec![1, 2, 3];

Это эквивалентно let v = vec![1, 2, 3];, но более кратко.

утверждать!

Макрос assert! проверяет, истинно ли условие, и паникует, если нет:

assert!(2 + 2 == 4);

Это используется для обеспечения инвариантов в вашей программе.

черт возьми!

Макрос dbg! выводит на консоль значение выражения и его тип:

dbg!(5);
// Prints:
// 5: i32

Это полезно для быстрой отладки значений в вашей программе.

паника!

Макрос panic! паникует и прерывает текущий поток:

panic!("Something went wrong!");

Вы можете предоставить тревожное сообщение, которое будет выведено на консоль.

недостижимо!

Макрос unreachable! помечает точку кода как недоступную. Если эта точка достигнута, макрос запаникует:

let x = 5;
match x {
    0 => println!("x is 0"),
    1 => println!("x is 1"),
    _ => unreachable!()  // This point should never be reached 
}

компиляция_ошибка!

Макрос compile_error! вызывает ошибку времени компиляции с данным сообщением:

compile_error!("This will not compile!");

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

IV. Пользовательские макросы

Мы можем определить наши собственные макросы для генерации кода во время компиляции. Существует несколько видов пользовательских макросов:

Макрос, похожий на функцию. Этот макрос раскрывается как вызов функции. Например, мы можем определить макрос double! для удвоения числа:

macro_rules! double {
    ($x:expr) => {
        $x * 2
    };
}

fn main() {
    let a = 5;
    let b = double!(a); // b = 10
}

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

macro_rules! sum {
    ($a:expr, $b:expr, $c:expr) => {
        $a + $b + $c 
    };
}

fn main() {
    let sum = sum!(1, 2, 3); // sum = 6
}

Макрос, который расширяется до нескольких операторов: Мы можем определить макрос unless!, который расширяется до оператора if-else:

macro_rules! unless {
    ($cond:expr) => {
        if !$cond { 
            // body
            }
    };
}

fn main() {
    unless!(true) {
        println!("Condition is false!"); 
    }
}

Итеративные макросы. Мы используем $(...)* для повторения элементов. Например, макрос repeat!:

macro_rules! repeat {
    ($a:expr; $($x:expr),*) => {
        $a
        $($a $x)* 
        $a
    };
}

fn main() {
    repeat!(println!("Hello"); 1, 2, 3); 
} 
// Prints:
// Hello 
// Hello 1 
// Hello 2 
// Hello 3
// Hello

Гигиеничные макросы. Макросы в Rust позволяют избежать случайных конфликтов имен за счет использования уникальных идентификаторов для совпадающих переменных. Это упрощает составление макросов.

Чтобы облегчить понимание статьи, я включил множество примеров и объяснений для каждого типа пользовательских макросов. Повторяющийся код генерируется с использованием списков уценок и заголовков, а примеры записываются в виде блоков кода для удобства чтения. Пожалуйста, дайте мне знать, если вы хотите, чтобы я объяснил или расширил какую-либо часть этого раздела. Я стремился написать увлекательную длинную статью, посвященную обнаруживаемости и SEO.

Ограничения макросов

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

  • Невозможно получить доступ к локальным переменным. Макросы не могут получить доступ к переменным, определенным в области, в которой они вызваны. Это связано с тем, что макросы расширяются в код до полной проверки типов и проверки заимствований.
  • ``rust let x = 10; some_macro!(x); // Cannot usex` здесь
  • fn foo() { let y = 20; какой-то_макрос!(у); // Также нельзя использовать y } ```
  • Невозможно определить функции в макросе. Вы не можете определять функции внутри тела макроса. Макросы могут расширяться только до выражений, элементов, операторов и токенов.
  • Для рекурсивных макросов требуется комбинатор с фиксированной запятой: Чтобы определить рекурсивный макрос, вам необходимо использовать комбинатор с фиксированной запятой, что может усложнить логику.
  • Невозможно получить доступ к полям вызываемых типов. Когда вы передаете структуру или перечисление в качестве аргумента макросу, вы не можете получить доступ к его полям внутри тела макроса. Это связано с тем, что макросы работают с абстрактным синтаксическим деревом (AST) до полной согласованности типов.
  • Макросы могут запутывать логику. Чрезмерно сложная логика макросов может затруднить понимание кода по сравнению с простыми вызовами функций. Поэтому, когда это возможно, отдавайте предпочтение простым функциям макросам.

Макросы — мощная часть Rust, но у них есть свои ограничения. Их разумное использование в нужных местах может помочь создать чистый, пригодный для повторного использования код, избегая при этом путаницы. Понимая, как работают макросы и их ограничения, вы сможете использовать их с максимальной эффективностью!

VI. Когда (не) использовать

Макросы — мощная функция Rust, но их следует использовать разумно. Вот несколько хороших вариантов использования макросов:

  • Генерация повторяющегося кода (шаблона):
macro_rules! generate_struct {
    ($name:ident, $($field:ident: $t:ty),*) => {
        struct $name {
            $($field: $t),* 
        }
    }
}

generate_struct!(User, name: String, age: u32);
  • Реализация шаблонов проектирования (шаблон наблюдателя):
macro_rules! subscribe {
    ($observer:ident) => {
        pub fn subscribe(&mut self, obs: $observer) {
            self.observers.push(obs);
        }
    } 
}

pub struct Subject {
    observers: Vec<Box<dyn Observer>> 
}

impl Subject {
    subscribe!(Observer);
}
  • Улучшение совместимости с C (с использованием макроса #[repr(C)])

Однако макросы могут затруднить понимание и отладку кода. Лучше избегать использования макросов, когда достаточно простых функций. Макросы также имеют некоторые ограничения:

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

Таким образом, используйте макросы разумно, когда они значительно улучшают выразительность, уменьшают повторение или позволяют метапрограммировать, но отдавайте предпочтение простым функциям, когда это возможно, из соображений ясности и модульности. С большой властью приходит большая ответственность!

Надеюсь, эта статья была для вас полезна! Если статья оказалась для вас полезной, поддержите меня: 1) нажмите несколько хлопков и 2) поделитесь этой историей в своей сети. Дайте мне знать, если у вас есть какие-либо вопросы по обсуждаемому содержанию.

Не стесняйтесь обращаться ко мне по адресу coderhack.com(at)xiv.in.