Макросы в Rust
Про макросы спрашивают дважды: сначала чем декларативные отличаются от процедурных, потом - когда их вообще стоит писать. Второй вопрос важнее, потому что чаще ошибаются именно в выборе инструмента.
Стержень: macro_rules! сопоставляет образцы, процедурный макрос это программа над токенами; и то и другое разворачивается при компиляции.
// Формулировки: «чем macro_rules отличается от proc-macro?», «что такое гигиена?», «когда макрос уместен?»
Два вида и гигиена
macro_rules! - набор правил «такой образец превращается в такой код». Он декларативный, живёт в обычном крейте и хорош для коротких удобств вроде vec!. Процедурный макрос - настоящая программа, получающая поток токенов и возвращающая новый; так работают derive, атрибуты вроде tokio::main и функциональные макросы. Живёт он в отдельном крейте и заметно дороже по времени сборки.
Гигиена означает, что имена, введённые внутри макроса, не конфликтуют с именами места вызова: объявленный там let x не затрёт ваш x. В C-препроцессоре это классический источник багов, здесь его нет. Гигиена частичная: типы и функции резолвятся в контексте вызова, поэтому внутри макросов пишут полные пути вроде ::std::vec::Vec.
- macro_rules!
- декларативный макрос по образцам
- гигиена
- изоляция имён макроса от имён места вызова
Когда макрос действительно нужен
Оправданных случаев немного: переменное число аргументов (vec!, println!), генерация кода по структуре типа (derive), проверки на этапе компиляции - например, разбор строки формата, где лишний аргумент становится ошибкой сборки.
Всё остальное лучше выражать функцией или дженериком. Макрос хуже читается, ломает подсказки IDE, усложняет диагностику ошибок и удлиняет сборку. И он ничего не даёт по скорости: обычные вызовы и так инлайнятся.
// Когда ошибка приходит из сгенерированного кода и непонятна, помогает cargo expand - он показывает, во что развернулся макрос.
- proc-macro
- процедурный макрос: программа над токенами
- cargo expand
- показ раскрытого кода макросов
Как отвечать: «Почему println! - макрос, а не функция?»
По двум причинам. Функция принимает фиксированный набор аргументов известных типов, а println! берёт сколько угодно значений разных типов. И главное - он проверяет строку формата на этапе компиляции: лишний аргумент, пропущенный плейсхолдер или тип без Display становятся ошибкой сборки, а не сюрпризом в рантайме. Обычной функцией такое не выразить. По той же причине макрос у vec!: переменный список элементов плюс форма с повтором.
Ты называешь не только переменную арность, но и проверку формата при компиляции - вторую половину ответа обычно забывают.
На чём валятся
- −Пишут макрос там, где хватило бы функции или дженерика.
- −Считают, что макрос ускоряет код, убирая накладные расходы вызова.
- −Забывают полные пути внутри макроса и ловят ошибку в чужом модуле.
- −Не знают, что процедурные макросы требуют отдельного крейта.
- −Разбираются в ошибке сгенерированного кода без cargo expand.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.
- Когда макрос — правильный инструмент, а когда лучше обойтись без него?A)Макрос — когда важна скорость: он убирает накладные расходы вызоваB)Макрос — для переменного синтаксиса и генерации по типуC)Макрос — когда нужно обойти ограничения системы типовD)Макрос — когда код повторяется больше трёх раз, независимо от вида повтора
показать ответ и разбор
+B)Макрос — для переменного синтаксиса и генерации по типу// разбор: Оправданные случаи: переменное число аргументов (vec!, println!), генерация impl по структуре (derive), встраивание проверок на этапе компиляции. Всё, что выражается функцией или дженериком, лучше писать ими: макрос хуже читается, ломает подсказки IDE, усложняет диагностику и удлиняет сборку. Скорости он сам по себе не добавляет — обычные вызовы и так инлайнятся.
- Почему println! и vec! — макросы, а не функции?A)Функции не умеют печатать в стандартный вывод без unsafeB)Так быстрее: макрос разворачивается в прямой системный вызовC)Им нужно переменное число аргументов и проверка форматаD)Функции не могут возвращать значения разных типов
показать ответ и разбор
+C)Им нужно переменное число аргументов и проверка формата// разбор: Функция в Rust принимает фиксированный набор аргументов известных типов, а println! берёт сколько угодно и проверяет соответствие строке формата ещё при компиляции: лишний аргумент или неверный тип — ошибка сборки, а не сюрприз в рантайме. vec! так же принимает произвольный список элементов и форму с повтором.
- Что означает запись $($x:expr),* в macro_rules!?A)Ровно один аргумент-выражение, который можно использовать многократно в телеB)Список типов, по которому макрос сгенерирует по реализации на каждый типC)Необязательный аргумент, который подставляется только при его наличииD)Повторение: ноль или больше выражений, разделённых запятыми
показать ответ и разбор
+D)Повторение: ноль или больше выражений, разделённых запятыми// разбор: Конструкция описывает повторяющуюся группу: $x — метапеременная с типом фрагмента expr, запятая — разделитель, звёздочка — ноль или больше повторов. В теле макроса ту же группу разворачивают через $(...)*. Плюс к этому обычно добавляют $(,)? — необязательную хвостовую запятую, чтобы vec![1, 2, 3,] тоже собирался.
- Какие три вида процедурных макросов бывают?A)Отладочный, релизный и универсальный — по профилю сборки крейтаB)Внутренний, экспортируемый и переэкспортируемый — по области видимостиC)derive, атрибутный и функциональный (вызываемый как макрос с восклицательным знаком)D)Синтаксический, семантический и оптимизирующий — по стадии компиляции крейта
показать ответ и разбор
+C)derive, атрибутный и функциональный (вызываемый как макрос с восклицательным знаком)// разбор: derive добавляет реализации по объявлению типа (#[derive(Serialize)]), атрибутный оборачивает и переписывает помеченный элемент (#[tokio::main], #[get("/")]), функциональный вызывается как макрос и получает произвольный поток токенов (sqlx::query!). Все три живут в отдельном крейте с типом proc-macro и работают с TokenStream на этапе компиляции.
- Проект собирается медленно, и профиль сборки показывает много времени в proc-macro крейтах. Почему так выходит?A)Процедурные макросы выполняются в рантайме программы и замедляют её запускB)Они компилируются под хост-платформу и запускаются на каждый обрабатываемый элемент, порождая много кодаC)Они пересобираются при каждом запуске cargo, потому что их результат не кэшируетсяD)Они блокируют параллельную сборку крейтов, выстраивая их в одну общую очередь
показать ответ и разбор
+B)Они компилируются под хост-платформу и запускаются на каждый обрабатываемый элемент, порождая много кода// разбор: Макрос — это программа, которую нужно собрать (вместе с syn и quote), а потом выполнить на каждом помеченном элементе; результат её работы — исходный код, который тоже надо разобрать и скомпилировать. Отсюда двойная цена: сборка самого макро-крейта и раздувание кода на выходе. Лечится точечно — убрать derive там, где он не нужен, или заменить генерацию на обычный код.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.