Перейти к содержимому

Макросы в Rust

Ещё на странице Синтаксис ты узнал простое правило: ! означает, что это макрос, а не обычная функция, — println!, vec!, format!. На этой странице мы наконец разберёмся, что это значит на самом деле.

Почему println! не может быть обычной функцией

Заголовок раздела «Почему println! не может быть обычной функцией»

Обычная функция вроде fn add(a: i32, b: i32) -> i32 всегда принимает фиксированное число аргументов фиксированных типов. А println! можно вызвать с совершенно разным количеством аргументов разных типов:

fn main() {
println!("здесь нет аргументов");
println!("{}", 42);
println!("{} и {}", "два", 2);
}

Вывод:

здесь нет аргументов
42
два и 2
▶ Попробуй сам

Никакая обычная сигнатура функции не приняла бы все эти вызовы. Макросы работают иначе: они срабатывают до того, как код полностью скомпилирован, и генерируют настоящий код на Rust в зависимости от того, как их вызвали. println!("{} и {}", "два", 2) по сути разворачивается в настоящий код, который обрабатывает ровно эти два аргумента, — и для каждого вызова получается своё развёртывание.

Свои макросы можно определять через macro_rules!. Вот макрос, который возводит число в квадрат:

macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(5));
}

Вывод:

25
▶ Попробуй сам

($x:expr) говорит: «прими одно выражение и назови его $x». Часть => { ... } — это код, в который разворачивается макрос: везде, где ты пишешь square!(5), компилятор подставляет 5 * 5 ещё до того, как начнёт что-либо компилировать.

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

macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
fn main() {
println!("{}", sum!(1, 2, 3));
println!("{}", sum!(10, 20));
}

Вывод:

6
30
▶ Попробуй сам

$($x:expr),* значит «ноль или больше выражений через запятую». Именно этот образец позволяет одному определению макроса работать и с vec![1, 2, 3], и с vec![1, 2, 3, 4, 5].

У макроса может быть несколько правил, разделённых точкой с запятой. Rust пробует их сверху вниз и использует первое подходящее. Правило может даже снова вызвать тот же макрос. Вот max!, который работает с любым числом аргументов:

macro_rules! max {
// Правило 1: одно значение. Самое большое — оно само.
($x:expr) => {
$x
};
// Правило 2: значение, а за ним ещё одно или несколько.
($x:expr, $($rest:expr),+) => {
{
let first = $x;
let biggest_of_rest = max!($($rest),+);
if first > biggest_of_rest { first } else { biggest_of_rest }
}
};
}
fn main() {
println!("{}", max!(7));
println!("{}", max!(3, 9, 4));
println!("{}", max!(2.5, 1.0));
}

Вывод:

7
9
2.5
▶ Попробуй сам

Для max!(3, 9, 4) правило 1 не подходит (значений больше одного), поэтому срабатывает правило 2. Оно откладывает 3 в сторону и разворачивает max!(9, 4), а тот, в свою очередь, разворачивает max!(4). Этот вызов подходит под правило 1, и на нём всё останавливается. Порядок важен: Rust всегда сначала пробует правило 1.

Здесь два новых кусочка синтаксиса:

  • + значит одно или больше, а * (из sum!) — ноль или больше. Правилу 2 нужно хотя бы одно значение после запятой, поэтому у внутреннего вызова max!(...) всегда есть с чем работать.
  • max! работает и с целыми числами, и с дробными, потому что макрос просто вставляет код first > biggest_of_rest. Функции для этого понадобились бы обобщения.

expr — не единственное, что может принимать макрос. Вот ещё несколько видов фрагментов, которые встречаются в реальных макросах:

Фрагмент Что подходит Пример
expr любое выражение 2 + 3, name.len()
ident имя (идентификатор) count, Point
ty тип i32, Vec<String>
literal литерал 42, "hi"

Ты будешь постоянно пользоваться макросами (println!, vec!, format!, #[derive(Debug)]), и при этом тебе вряд ли понадобится писать свои. Собственные макросы — узкий навык, которым в основном пользуются авторы библиотек, когда нужно убрать действительно повторяющийся шаблонный код в большой кодовой базе. Полезно знать, что они есть и примерно как работают, но браться за них на старте не нужно.

Напиши макрос print_all!, который принимает любое количество значений через запятую и печатает каждое на отдельной строке. print_all!("яблоко", 42, 3.5) должен напечатать три строки. Подсказка: сопоставь $($x:expr),*, как в sum!, и повтори println! для каждого значения с помощью $( ... )*.

Показать решение
macro_rules! print_all {
($($x:expr),*) => {
$(println!("{}", $x);)*
};
}
fn main() {
print_all!("яблоко", 42, 3.5);
}

Вывод:

яблоко
42
3.5
▶ Попробуй сам

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

Напиши my_vec!, который работает как vec!. my_vec![] даёт пустой Vec, а my_vec![1, 2, 3] — Vec с этими элементами. Подсказка: используй два правила. Первое не принимает вообще ничего: (). Второе сопоставляет $($x:expr),+, создаёт Vec::new() и кладёт в него каждый $x.

Показать решение
macro_rules! my_vec {
() => {
Vec::new()
};
($($x:expr),+) => {
{
let mut v = Vec::new();
$(v.push($x);)+
v
}
};
}
fn main() {
let empty: Vec<i32> = my_vec![];
let numbers = my_vec![1, 2, 3];
println!("{:?} {:?}", empty, numbers);
}

Вывод:

[] [1, 2, 3]
▶ Попробуй сам

Любой макрос можно вызвать с (), [] или {}. Для макросов-списков вроде vec! принято использовать квадратные скобки, но my_vec!(1, 2, 3) тоже работает. Пустому Vec нужен тип : Vec<i32>: без элементов Rust не может понять, что в нём будет лежать.

  • Макрос разворачивается в настоящий код до компиляции — поэтому println! может каждый раз принимать разное число аргументов разных типов.
  • ! после имени — всегда признак того, что ты вызываешь макрос, а не функцию.
  • macro_rules! позволяет определять свои макросы с образцами вроде одного выражения ($x:expr) или повторяющегося списка ($($x:expr),*).
  • У макроса может быть несколько правил. Rust пробует их сверху вниз, и правило может снова вызвать тот же макрос.
  • Писать макросы — узкий навык: макросов из стандартной библиотеки хватает почти для всего, что тебе понадобится.

Проверь себя

1. Почему println! нельзя написать как обычную функцию?

2. Что значит $x:expr внутри образца macro_rules!?

3. Когда макросы разворачиваются в настоящий код?

Результат: 0 / 3