一句话总结
声明宏(macro_rules!)是"代码的查找替换引擎":它在编译早期按写好的模式匹配输入的代码片段,再展开成另一段代码.
它能做到函数做不到的事--接受任意数量参数,操作类型本身,生成重复结构.
前置回顾
前面的迭代器和闭包两篇,涉及了大量标准库宏的使用:vec!,println!,format!.本篇揭开宏的面纱:这些以 ! 结尾的调用到底是怎么工作的,以及如何自己写一个.
为什么需要宏:函数的天花板
vec!,println!,format! 末尾的感叹号就是宏的标志.先想清楚一个问题:为什么 vec! 是宏,而不是函数?
|
普通函数的参数个数是固定的,无法写一个能接收"任意多个参数"的 vec 函数.宏却可以,因为它工作在语法层面--它看到的是代码本身,能根据写了几个元素,生成对应数量的 push 调用.
| 能力 | 函数 | 宏 |
|---|---|---|
| 可变参数个数 | 不支持 | 支持 |
| 操作类型作为参数 | 不支持 | 支持 |
| 生成 impl,结构体等定义 | 不支持 | 支持 |
| 编译期展开,无运行时开销 | - | 是 |
| 易读,易调试 | 好 | 较差 |
函数是运行时调用的工人,拿到具体的值再干活.
宏是编译时的代码生成器,更像一套"文本模板 + 填空规则",在程序还没跑起来之前,先把模板展开成真正的代码.
宏在编译的哪个阶段展开
理解宏,关键要知道它发生在什么时候.Rust 编译大致经过:源码 → 词法分析(切成 token)→ 宏展开 → 语法树 → 类型检查 → 代码生成.
|
宏展开在类型检查之前
宏处理的是 token(语法片段),而非具体的值,且发生在类型检查之前.
这解释了两个现象:其一,宏能接受还没"类型化"的东西,比如一个类型名,一个表达式片段;其二,宏展开后的代码若有类型错误,编译器报错位置会指向展开结果,有时不太直观--这正是宏比函数难调试的根源.
基本语法:像 match 一样匹配
声明宏的结构和 match 表达式高度相似,左边是匹配器(matcher),右边是展开模板(transcriber).
|
带参数的版本,需要用元变量(metavariable) 捕获输入片段,语法是 $名字:类型:
|
片段说明符:捕获什么样的代码
元变量后面的"类型"称为片段说明符(fragment specifier),它规定能捕获哪一类语法片段.常用的有:
| 说明符 | 匹配的内容 | 例子 |
|---|---|---|
expr |
表达式 | 1 + 2,foo() |
ident |
标识符(变量/函数名) | x,my_fn |
ty |
类型 | i32,Vec<u8> |
literal |
字面量 | 42,"hi" |
stmt |
语句 | let x = 1 |
tt |
单个 token 树(最灵活) | 几乎任何东西 |
expr 会被当作整体,自动加括号
当用 $x:expr 捕获到 3 + 1 后,在模板里展开时它被视为一个完整表达式整体,相当于自动套了括号.所以 square!(3 + 1) 展开成 (3+1)*(3+1) 而非 3+1*3+1.
这是宏卫生性设计的一部分,避免了 C 语言宏里经典的运算符优先级陷阱.
重复:处理任意数量参数
宏真正强大之处,是用重复(repetition) 语法处理可变数量的输入.这正是 vec! 的实现核心.语法是 $(...)分隔符重复标记:
|
重复标记有三种:
*:重复零次或多次+:重复一次或多次?:出现零次或一次
重复语法像一个"复印 + 套用"的流水线:$( ... )* 是模板,编译器对传入的每一项,都按模板复印一份并把对应的值填进去.传三个值,就复印三份 push.
宏卫生性:不会污染变量
C 语言的宏臭名昭著,因为宏内部定义的变量可能意外覆盖调用处的同名变量.Rust 宏通过卫生性(hygiene) 机制根除了这个问题.
|
卫生性 = 宏内外的标识符互不干扰
Rust 编译器为宏展开引入的标识符打上特殊的"语法上下文"标记,使宏内部的 tmp 与调用处的 tmp 即便同名,也被视为不同的变量.用别人的宏时,不必担心它偷偷改坏局部变量.这是 Rust 宏相比 C 宏的一大安全优势.
如何调试宏
宏出错时报错信息常指向展开后的代码,不直观.两个实用工具:
|
声明宏的能力边界
macro_rules! 虽强,但它只能做"基于模式的语法替换",无法执行任意逻辑(如读取文件,解析复杂语法,根据字段名生成代码).
当需求超出模式匹配--比如实现 #[derive(Debug)] 这种要遍历结构体字段的功能--就得升级到过程宏,这正是下一篇的主题.
快速回顾
- 宏突破函数的限制:可变参数,操作类型,生成定义,编译期展开无运行时开销
- 展开时机:在词法分析之后,类型检查之前,处理的是 token 而非值
- 结构像 match:左侧匹配器(含元变量 + 片段说明符),右侧展开模板
- 重复语法
$(...)分隔符*处理任意数量参数,是vec!的核心 - 卫生性保证宏内外同名标识符互不干扰,杜绝 C 宏式的变量污染
动手练习
- max! 宏:实现一个
max!宏,接受两个表达式返回较大者;再扩展成支持任意多个参数(提示:用重复 + 递归调用宏自身). - hashmap! 宏:用
$key:expr => $val:expr配合重复语法,写一个hashmap!宏,让hashmap!{"a" => 1, "b" => 2}构造出一个HashMap. - cargo expand:安装
cargo-expand,对写好的hashmap!调用执行cargo expand,查看真实展开结果.