本节摘要:声明宏是"模式 → 模板"的批量生成器:按书写顺序匹配输入的标记流,命中即展开为模板代码。本节拆解规则语法、重复模式与卫生宏概念,并手写两个实用宏。读完你能读懂生态里的声明宏并动手写自己的。

macro_rules! announce { ($name:expr) => { println!("现在宣布:{}", $name); }; } announce!("驳回上诉"); // 展开成上面那行 println
$name:expr 是"捕获一个表达式,命名为 name"。匹配按分支顺序尝试,先到先得。函数做不到这件事:参数是运行时的值,而宏在编译期就看到了"println 加格式串加参数"的完整结构,所以格式错误能在编译期报出——第 1 章的悬念至此了结。
macro_rules! my_vec { ($($x:expr),* $(,)?) => { { let mut v = Vec::new(); $(v.push($x);)* v } }; } let exhibits = my_vec!["指纹", "鞋印", "卷尺"];
$($x:expr),* 读法:重复捕获表达式,逗号分隔,重复零次或多次;$(,)? 允许尾逗号;展开侧的 $(...)* 与捕获侧一一对应。这就是标准库 vec! 宏的核心骨架——它不过是"重复 push"的批量生成。
宏内部声明的变量不会与调用处撞名:展开时编译器给宏里的标识符打上独立标记,let mut v 绝不会吃掉调用处的同名变量。这避免了 C 宏最臭名昭著的一类事故,也让声明宏不适合"需要注入变量名"的场合——那种需求交给下一节的过程宏。
宏匹配的不是字符串而是语法片段,九个说明符各对应一类语法单元,选错说明符是最常见的驳回原因。
macro_rules! exhibit { // expr:表达式 —— item:项 —— ty:类型 —— ident:标识符 // literal:字面量 —— pat:模式 —— block:块 —— path:路径 —— tt:单棵语法树 ($id:ident holds $count:expr literal $lit:literal) => { { let mut $id = Vec::new(); for _ in 0..$count { $id.push($lit); } $id.len() as u32 } }; } fn main() { let n = exhibit!(box_a holds 3 literal "证"); println!("{} 件", n); // 3 件 }
| 说明符 | 捕获对象 | 典型用途 |
|---|---|---|
| expr | 表达式 | 参数、配置值 |
| ident | 标识符 | 生成变量/函数名 |
| ty | 类型 | 泛型化样板 |
| pat | 模式 | 自定义 match 包装 |
| tt / token 树 | 任意单语法树 | 递归展开、转发剩余内容 |
expr 捕获有一个出名的边界:捕获后的表达式在展开处会自动加括号防优先级事故,但作为语句使用时可能触发 "temporary value dropped" 类悬垂——遇到时改用 &$e 或绑定局部变量再展开。
$(...),* 与分隔符组合是批量生成的核心语法,重数变量用下标访问。
macro_rules! count_claims { () => { 0usize }; ($head:tt $(, $rest:tt)*) => { 1usize + count_claims!($($rest),*) }; // 递归降维 } macro_rules! dispatch { ($($name:ident => $body:block),+ $(,)?) => { // + 至少一次,尾逗号可选 $(fn $name() -> u32 $body)+ }; } dispatch! { open => { 1 }, close => { 2 }, } fn main() { let total = open() + close(); println!("{} 项 {}", total, count_claims!(a, b, c, d)); }
第二例一次生成了两个函数,宏的"批量法条"之名正源于此。递归展开(第一例)用来实现变长逻辑,编译器对递归深度有上限,超限报 "recursion limit reached",可用属性调高,但通常是设计信号。
声明宏默认卫生:展开体内引入的变量与调用处变量互不可见,杜绝"宏内部的 tmp 撞上用户的 tmp"。需要在调用处声明变量时,必须让标识符来自宏参数(如上面的 $id:ident),由调用方提供名字,宏只负责接线。这条规则初学时像限制,工程上是宏可组合的根基。
声明宏报错信息只指向调用点,定位三板斧:第一板,把输入缩小到最小复现,通常一两行就能逼出"no rules expected the token"的具体分支;第二板,在可疑分支前加一条兜底 ($t:tt)* => { compile_error!("走到了兜底") } 观察实际落入哪条规则;第三板,用外部展开工具看产物形态。宏的复杂度成本主要在维护端,写的时候多花十分钟设计好规则顺序(最特殊的在前,兜底在后),调试时能省一小时。
把"声明一堆测试函数"或"注册处理器表"的样板宏化,是声明宏在工程里最高频的合法席位,完整判例如下。
macro_rules! handlers { ( $( $name:ident : $key:expr => $body:expr ),+ $(,)? ) => { pub fn dispatch(key: &str) -> Option<u32> { match key { $( $key => Some($body()), )+ _ => None, } } $( pub fn $name() -> u32 { $body } )+ }; } handlers! { open_case : "open" => || 1, close_case: "close" => || 2, } fn main() { println!("{:?}", dispatch("open")); // Some(1) println!("{:?}", dispatch("other")); // None }
一个宏同时生成了 match 分派与独立函数两套产出,这正是"重复语法结构"的典型样本:逻辑用函数写不出(match 与函数声明都是语法级样板),宏恰好各就各位。注意到 match 分支里 $key 是 expr——字符串字面量在模式位置可用,这是字面量说明符的便利;若键是变量表达式则需改设计,模式位置只吃字面量或路径。