9.1 声明宏:macro_rules展开规则


9.1 声明宏:macro_rules 展开规则

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

图:宏调用到展开的三步

图:宏调用到展开的三步

宏不是函数

macro_rules! announce { ($name:expr) => { println!("现在宣布:{}", $name); }; } announce!("驳回上诉"); // 展开成上面那行 println

$name:expr 是"捕获一个表达式,命名为 name"。匹配按分支顺序尝试,先到先得。函数做不到这件事:参数是运行时的值,而宏在编译期就看到了"println 加格式串加参数"的完整结构,所以格式错误能在编译期报出——第 1 章的悬念至此了结。

分支与重复:vec! 的骨架

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 宏最臭名昭著的一类事故,也让声明宏不适合"需要注入变量名"的场合——那种需求交给下一节的过程宏。

要点回顾

  • 声明宏按分支顺序匹配标记流,先命中先展开;
  • 重复语法是主力武器,vec! 就是活教材;
  • 格式检查等编译期能力是宏区别于函数的根本;
  • 卫生性自动隔离变量,安全性内建。

片段说明符:宏的语法分类学

宏匹配的不是字符串而是语法片段,九个说明符各对应一类语法单元,选错说明符是最常见的驳回原因。

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——字符串字面量在模式位置可用,这是字面量说明符的便利;若键是变量表达式则需改设计,模式位置只吃字面量或路径。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U