宏:声明宏 + 过程宏 过程宏:
- 派生宏
- 属性宏
- 函数宏
声明宏
声明宏(Declarative Macros),也就是我们常用的 macro_rules!,本质上是一个基于模式匹配的编译期“文本/代码替换”引擎。它不关心运行时的数据,只关心编译期的语法结构(Tokens)。
一、 声明宏核心机制:把代码当数据匹配
声明宏就像是针对 Rust 源代码的 match 语句。你定义一些“模式(Pattern)”,当编译器看到你调用宏时,就会拿你的输入去套这些模式,匹配成功就进行代码展开。
macro_rules! my_macro {
// 模式 => 展开后的代码
($x:expr) => {
println!("你传了一个表达式: {}", $x);
};
}
$x被称为元变量(Meta-variable)。expr被称为片段分类符(Fragment Specifier) 或 指示符(Designator),它决定了$x能够强行捕获什么样语法的代码。
二、 捕获标识符的类型有哪些
有一个至关重要的概念需要澄清:在声明宏内部,你无法像在运行时那样通过逻辑去“查询或判断”捕获进来的标识符到底是什么类型。 因为宏在编译的最早期(AST 构建阶段)就已经展开完毕了,那时候连 Rust 的类型系统(比如 i32、String)都还没开始工作。
如果你想在开发或调试时查询/检查宏到底捕获了什么、展开成了什么,有以下三种主流的高级手段:
1. 编译期打印:利用 stringify! 宏
如果你想看宏捕获到的代码片段在字面上长什么样,可以使用内置的 stringify!,它能在编译期把任何捕获的片段变成纯字符串:
macro_rules! inspect {
($name:ident, $val:expr) => {
// stringify! 会把传入的标记转换为字符串字面量
println!("变量名是: {}, 它的表达式代码是: {}", stringify!($name), stringify!($val));
};
}
2. 终极调试大招:使用 cargo-expand
这是每个写宏的 Rust 程序员必备的工具。它能让你看到宏展开后的最终裸代码。 在终端运行:
cargo expand
3. 运行时查询真实的 Rust 类型
如果你想知道被捕获的变量在 Rust 类型系统里到底是什么类型(例如是 i32 还是 u32),你必须借助运行时的反射工具:
macro_rules! check_type {
($val:expr) => {
// 利用标准库在运行时打印真实的 Rust 类型系统类型
println!("Rust 真实类型是: {}", std::any::type_name_of_val(&$val));
};
}
三、 捕获指示符(Fragment Specifiers)全家桶
当你在宏里写 $变量名:指示符 时,Rust 一共提供了 14 种 指示符来限制和捕获不同层级的语法树节点。
以下是截至当前最新 Rust 版本的完整类型清单:
| 指示符 (Specifier) | 允许捕获的内容 | 典型示例 |
|---|---|---|
ident | 标识符(变量名、函数名、结构体名、关键字等) | x, foo, MyStruct, async |
expr | 任何合法的表达式(有返回值的代码段) | 2 + 2, my_func(), if true { 1 } else { 0 } |
stmt | 单条语句(通常不含末尾分号,除非是 item 语句) | let x = 5 |
block | 用花括号包裹的代码块 | { let a = 1; a + 1 } |
pat | 任何模式(常见于 match 或 if let 的左侧) | Some(x), _, 1..=5, ref mut y |
pat_param | 模式参数(行为与 pat 类似,但在某些历史上下文中限制更少) | Some(x) |
ty | Rust 类型 | i32, Vec<String>, &'static str |
path | 路径(用于定位模块、类型或具体函数) | std::collections::HashMap, crate::foo::bar |
literal | 字面量常量 | 42, "hello", true, 3.14, -1 |
item | 一个条目定义(整个函数、结构体、模块等定义) | fn hello() {}, struct User;, pub use crate::x; |
meta | 属性宏内部的元信息(#[...] 里面的内容) | derive(Debug, Clone), cfg(target_os = "linux") |
lifetime | 生命周期标记或标签 | 'a, 'static |
vis | 可见性修饰符(可能为空) | pub, pub(crate) |
tt | 标记树 (Token Tree):最强万能牌。可以是单张标记(如 +、5),或者任何被 (), [], {} 正确包裹的对称代码块。 | (a + b * c), anything! |
四、 实战演练:多类型捕获的声明宏
下面这个宏展示了如何在同一个宏中组合使用多种指示符来生成一个高仿的“属性/方法”注入器:
macro_rules! create_method {
// 捕获可见性 $vis,函数名 $name,返回类型 $ret,以及具体实现的块 $body
($vis:vis fn $name:ident() -> $ret:ty $body:block) => {
$vis fn $name() -> $ret {
println!("--- 宏注入:开始调用 {} ---", stringify!($name));
// 将整个捕获的代码块原封不动塞进来执行
let result = $body;
println!("--- 宏注入:结束调用 ---");
result
}
};
}
// 使用宏
create_method! {
pub fn get_magic_number() -> i32 {
let x = 21;
x * 2
}
}
fn main() {
// 此时 get_magic_number 已经被宏优雅地编译出来了
let num = get_magic_number();
println!("得到数字: {}", num);
}
exprvstt:如果你只想把一段代码原封不动地传来传去,优先用tt(Token Tree)。因为expr一旦捕获,它在宏内部就会被打包成一个不可分割的“表达式整体”,之后再进行某些特定的语法拼接时可能会引发编译报错。- 跟随规则(Follow-set Restrictions):为了防止语法产生歧义,Rust 限制了某些指示符后面能跟着什么字符。比如
$e:expr后面只能跟着"=>",、;等少数分隔符。如果你发现宏报错说“local ambiguity(局部歧义)”,通常就是因为两个捕获靠得太近,编译器分不清边界了。