Макросы в 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));}Вывод:
630$($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));}Вывод:
792.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)]), и при этом тебе вряд ли понадобится писать свои. Собственные макросы — узкий навык, которым в основном пользуются авторы библиотек, когда нужно убрать действительно повторяющийся шаблонный код в большой кодовой базе. Полезно знать, что они есть и примерно как работают, но браться за них на старте не нужно.
Практика
Заголовок раздела «Практика»1. Вывести всё
Заголовок раздела «1. Вывести всё»Напиши макрос 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);}Вывод:
яблоко423.5Макрос разворачивается в три отдельных вызова println!, по одному на аргумент. У каждого значения может быть свой тип — обычная функция так бы не смогла.
2. Свой vec!
Заголовок раздела «2. Свой vec!»Напиши 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
Что почитать
Заголовок раздела «Что почитать»- Макросы — книга «Язык программирования Rust» (перевод на русский): как определён
vec!, а также введение в процедурные макросы вроде#[derive]. macro_rules!— Rust на примерах (перевод на русский): короткие запускаемые примеры обозначений (designators), перегрузки и повторений.- Macros By Example — The Rust Reference (на английском): полный список типов фрагментов вроде
expr,identиttи то, как именно работает сопоставление. - The Little Book of Rust Macros (на английском): бесплатное и дружелюбное глубокое погружение для тех, кто хочет писать большие макросы.