声明式宏的定义
1.语法结构
声明式宏允许我们写出类似 match 的代码。不同的是,定义宏需要使用macro_rules!来匹配一个宏名,后面可以有多个匹配臂
macro_rules! 宏名 {
// 匹配臂(matcher => transcriber)
(模式1) => { 展开代码1 };
(模式2) => { 展开代码2 };
// ...可以有任意多个臂
}每个匹配臂由两部分组成:
- 左侧(matcher,匹配器):描述这个宏能接受什么样的输入 token。
=>:箭头,分隔匹配器和转换器。- 右侧(transcriber,转换器/展开体):当左侧匹配成功时,替换成的代码。
每个臂以分号 ; 结尾(最后一个臂的分号可省略,但建议写上)。
宏的调用方式是 宏名!(参数)。
2.匹配器语法元素
匹配器里可以出现以下几类东西:
2.1 字面量 token
可以直接写出任何 Rust 的标点符号、关键字、标识符,它们必须原样匹配。
macro_rules! m {
(struct $name:ident) => { ... }; // 必须第一个 token 是 `struct`
}
m!(struct Foo); // ✅
m!(enum Foo); // ❌ 不匹配常见的字面量 token:struct、fn、let、=、->、;、+、*、(、)、[、]、{、}、:、=>、@、|、&、<、> 等等。
2.2 捕获符
语法:$名字:片段说明符
$:表示"这是一个要捕获的变量"。名字:给捕获的内容起个名字,方便在右侧用$名字引用。::分隔名字和片段说明符。- 片段说明符(fragment specifier):告诉编译器"这里期望匹配什么语法结构"。
下面是 Rust 支持的所有片段说明符,逐个解释含义和可匹配的语法:
| 说明符 | 名称 | 能匹配的内容 | 典型用途 |
|---|---|---|---|
item | 项 | 任意 Rust 项(函数、struct、enum、impl、mod、trait、static、const、use 等) | 注入整个项 |
block | 块 | 大括号包围的块表达式 { ... } | 包裹代码块 |
stmt | 语句 | 一条语句(如 let x = 1;、foo();,注意不带结尾分号的块语句除外) | 插入语句 |
pat | 模式 | 一个模式(如 Some(x)、(a, b)、ref mut x),edition 2021 起较宽松 | 解构绑定 |
pat_param | 参数模式 | 旧版 pat 行为(不能顶层 |、不能 @ 子模式),edition 2021 新增 | 兼容旧代码 |
expr | 表达式 | 任意表达式(1 + 2、foo()、if .. {}、match .. {} 等) | 计算值 |
ty | 类型 | 任意类型(i32、Vec<u8>、&str、impl Trait) | 泛型/类型注解 |
ident | 标识符 | 标识符或关键字(foo、Bar、self 等) | 生成函数/变量名 |
path | 路径 | 路径(std::collections::HashMap、crate::foo) | 调用路径 |
tt | token 树 | 单个 token 或一对定界符包围的一组 token((..)、[..]、{..} 之一) | 最灵活的捕获 |
meta | 元项 | 属性内部的内容(cfg(target_os="linux")、derive(Debug)) | 处理属性 |
lifetime | 生命周期 | 生命周期标注('a、'static) | 泛型生命周期 |
vis | 可见性 | 可选的可见性修饰符(pub、pub(crate)、pub(in path),也可为空) | 生成带可见性的项 |
literal | 字面量 | 字面量表达式("str"、'c'、42、3.14、true、b"bytes") | 常量值 |
item
匹配任意项
macro_rules! inject {
($i:item) => {
$i
};
}
inject! {
fn hello() { println!("hi"); }
}block
匹配代码块
macro_rules! run {
($b:block) => {
$b
};
}
run!({ println!("a"); println!("b"); });stmt
匹配语句
macro_rules! do_stmt {
($s:stmt) => {
$s
};
}
do_stmt!(let x = 5;);
do_stmt!(println!("hi"););pat 和 pat_param
匹配模式
macro_rules! match_pat {
($p:pat) => {
let $p = Some(10);
println!("{:?}", $p);
};
}
match_pat!(x); // x = Some(10)
match_pat!((a, b)); // (a, b) = Some(10)关于 pat vs pat_param(edition 差异):
- edition 2018 的
pat实际上等价于现在 2021 的pat_param(受限)。 - edition 2021 引入
pat_param(受限版),同时放宽了pat,允许顶层或模式|和@子模式:
// edition 2021 下,pat 允许这样:
macro_rules! p {
($x:pat) => { ... };
}
p!(A | B); // ✅ edition 2021 的 pat 允许顶层 |
p!(x @ 1..=10); // ✅如果你需要旧的限制行为,用 pat_param。
expr
匹配表达式
macro_rules! square {
($e:expr) => {
{
let val = $e;
val * val
}
};
}
let r = square!(2 + 3); // (2+3)*(2+3) = 25tr
匹配类型
macro_rules! make_vec {
($t:ty) => {
Vec::<$t>::new()
};
}
let v = make_vec!(i32);ident
匹配标识符
macro_rules! gen_fn {
($name:ident) => {
fn $name() { println!("generated"); }
};
}
gen_fn!(my_func);
my_func();path
匹配路径
macro_rules! call_default {
($p:path) => {
$p::default()
};
}
let x: Vec<i32> = call_default!(Vec);tt
匹配 token 树(最灵活)
tt 匹配单个 token 或 一对定界符及其内容(圆括号 ()、方括号 []、花括号 {} 之一)。
macro_rules! dbg_tt {
($t:tt) => {
println!(stringify!($t));
};
}
dbg_tt!(hello); // hello
dbg_tt!([1, 2]); // [1, 2]
dbg_tt!({ a; b }); // { a ; b }tt 常用于"吞掉"一大段不想解析的代码,再原样吐出来。它是递归宏的基础。
meta
匹配属性元项
macro_rules! with_attr {
(#[$m:meta]) => {
#[$m]
fn annotated() {}
};
}
with_attr!(#[cfg(test)]);
with_attr!(#[allow(dead_code)]);lifetime
macro_rules! ref_type {
($lt:lifetime, $t:ty) => {
&$lt $t
};
}
fn f<'a>(x: ref_type!('a, i32)) {}vis
匹配可见性(可为空)
macro_rules! gen_struct {
($v:vis $name:ident) => {
$v struct $name { field: i32 }
};
}
gen_struct!(pub Foo); // pub struct Foo { ... }
gen_struct!(Bar); // (私有)struct Bar { ... }literal
匹配字面量
macro_rules! const_str {
($l:literal) => {
const MSG: &str = $l;
};
}
const_str!("hello world");3.重复语法
3.1 基本语法
这是宏最强大的特性之一,用来处理"可变数量"的参数。基本语法:
$( 模式 ) 分隔符? 重复操作符$( ... ):标记一段"可重复"的模式。分隔符(可选):比如
,、;、:等 token,用于分隔每次重复。重复操作符(必选,三者之一):
*:重复 0 次或多次+:重复 1 次或多次?:重复 0 次或 1 次
示例
// 匹配 0 个或多个用逗号分隔的表达式
macro_rules! sum {
($($x:expr),* $(,)?) => {
0 $(+ $x)*
};
}
let s = sum!(1, 2, 3, 4); // 0 + 1 + 2 + 3 + 4 = 10
let z = sum!(); // 0$(,)? 表示"可选的末尾逗号",让 sum!(1,2,3,) 也能工作。
// ? 用法:可选的类型注解
macro_rules! opt_ty {
($name:ident $(: $t:ty)?) => {
let $name $(: $t)? = 0;
};
}
opt_ty!(x); // let x = 0;
opt_ty!(y: i32); // let y: i32 = 0;3.2 嵌套重复
重复里面还能再套重复,用于处理多维结构(如二维数组字面量):
macro_rules! matrix {
($([$($e:expr),*]),*) => { /* ... */ };
}
matrix!([1,2,3], [4,5,6]);3.3 转换器侧的重复
右侧展开时,如果用了 $(...)*,里面的 $变量 必须来自同一层(或外层)的重复。换句话说,重复的"形状"必须匹配。
macro_rules! pair {
($($a:expr, $b:expr),*) => {
// 这里 $a 和 $b 都在同一个 * 重复里,所以可以一起重复
$( println!("{} {}", $a, $b); )*
};
}
pair!(1, "a", 2, "b");合法:$a 和 $b 都来自同一组 * 重复。
非法示例:
macro_rules! bad {
($($a:expr),* ; $($b:expr),*) => {
$($a + $b)* // 错误:$a 和 $b 来自不同的重复,无法配对
};
}3.4 元变量表达式
rust 1.70+ 稳定了一些高级表达式,写在 ${...} 里(注意不是 $(...)):
| 表达式 | 含义 |
|---|---|
${index()} | 当前重复的下标(从 0 开始) |
${count(x)} | 捕获变量 x 的重复次数 |
${ignore(x)} | 引用 x 但不参与重复匹配检查(用于占位) |
${concat(a, b)} | 拼接标识符(如 a 和 b 拼成新 ident) |
${length("abc")} | 字符串字面量的长度 |
macro_rules! indexed {
($($x:expr),*) => {
$(
println!("index {} = {}", ${index()}, $x);
)*
};
}
indexed!(10, 20, 30);
// index 0 = 10
// index 1 = 20
// index 2 = 304.跟随集歧义限制
当一个捕获符是 expr、stmt、ty、pat/pat_param 这几种类型时,它后面紧跟的 token 受到限制——只能跟"该语法结构自然结束之后允许出现的 token"。
具体来说(经典规则):
expr和stmt之后,只允许跟:=>,,,;ty和pat之后,允许跟:=>,,,;,:,>,>>,[,{,as,where等
因为 Rust 的宏解析器是贪心且无回溯的。如果允许 $e:expr 后面跟 +,那么 1 + 2 到底是把 1 当 expr 然后 + 是后面的 token,还是把 1 + 2 整个当 expr?编译器无法确定,所以干脆禁止。
常见报错示例
错误:ty 后面跟 +
macro_rules! bad {
($t:ty +) => { ... }; // 编译错误:`+` cannot follow `ty`
}错误:expr 后面跟 +
macro_rules! bad {
($e:expr + $f:expr) => { ... }; // 错误
}错误:多臂歧义(局部歧义)
macro_rules! ambiguous {
($t:ty) => { 1 };
($t:ty, $u:ty) => { 2 };
}
ambiguous!(u32); // 错误:解析完 u32 后不知道走哪个臂解决办法:用 tt 或调整设计,或者加分隔符。例如想要"类型列表",用定界符包起来:
macro_rules! ok {
(< $($t:ty),* >) => { ... };
}
ok!(<i32, u8, String>);TIP
- 如果你的宏需要"后面跟任意 token",把那段用
tt捕获,或者用定界符()/[]/{}包住。 - 遇到 "local ambiguity" 或 "follow-set" 错误时,通常是
expr/ty后面跟了不该跟的符号,加定界符或换tt即可。
5.宏卫生性
Rust 宏是卫生的(hygienic):宏内部定义的标识符不会意外"泄漏"或与外部同名变量冲突。
macro_rules! make_var {
() => {
let x = 99; // 这个 x 是宏"内部"的,和外部的 x 不是同一个
println!("{}", x);
};
}
fn main() {
let x = 1;
make_var!(); // 打印 99
println!("{}", x); // 打印 1,外部的 x 没被改
}- 宏捕获进来的变量(
$x:expr传入的)保留调用处的语义。 - 宏自己写的标识符(如上面
let x = 99里的x)属于宏的定义上下文,与外部隔离。
好处:不会意外捕获/遮蔽。坏处:有时你想在宏里引用调用处的某个 helper,需要用 crate:: 绝对路径或 $crate 伪变量。
5.1 $crate 伪变量
在 #[macro_export] 的宏里,用 $crate 指代"宏所在的 crate",避免用户调用时路径解析错误:
#[macro_export]
macro_rules! my_assert {
($cond:expr) => {
if !$cond {
$crate::helper::panic(); // 指向宏所在 crate 的 helper
}
};
}6. 作用域与可见性
6.1 默认作用域
macro_rules! 定义的宏只在定义点之后的当前模块及子模块可见(类似 let 的作用域,但作用于宏命名空间)。
macro_rules! m { () => {}; }
m!(); // ✅ 同作用域可用
// 在另一个模块里默认看不到 m!,除非用 #[macro_use] 或路径6.2 #[macro_export]
加在宏上,把它导出到 crate 根,并可供外部 crate 使用:
#[macro_export]
macro_rules! my_macro {
() => {};
}外部使用:use my_crate::my_macro;,然后 my_macro!();
6.3 #[macro_use]
#[macro_use] extern crate foo;:把foocrate 里#[macro_export]的宏导入当前作用域(edition 2015 常用,2018+ 推荐用use)。#[macro_use] mod bar;:把bar模块里定义的宏导入父模块作用域。
6.4 local_inner_macros
当你在一个 #[macro_export] 宏内部调用另一个本地宏时,加上 #[local_inner_macros] 可以让内部宏调用解析到本 crate 的同名宏,而不是用户的:
#[macro_export]
#[local_inner_macros]
macro_rules! outer {
() => { inner!() }; // 这里的 inner! 指向本 crate 的 inner
}7. 新版 macro 关键字
(Macros 2.0,edition 2021+)除了 macro_rules!,Rust 还提供了更现代的 macro 定义方式,遵循普通的模块可见性规则:
pub macro my_macro($x:expr) {
println!("{}", $x)
}
// 使用
my_macro!(123);特点:
- 用
pub/pub(crate)等控制可见性,像普通fn一样。 - 不需要
#[macro_export],直接use即可。 - 目前功能上和
macro_rules!等价,但语法更干净、作用域更直观。 - 仍属于声明式宏,不支持过程宏的能力。
