Rust 的宏系统很强大,但语法也比较特殊。这篇整理声明宏 macro_rules! 的基本用法和常见模式。
声明宏基本语法
声明宏用 macro_rules! 定义,本质是模式匹配 + 代码替换:
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!(); // 展开为 println!("Hello!");
}
括号内是一组规则,每条规则的形式是 (匹配模式) => { 展开代码 };。
模式匹配的类型
宏模式中用 $name:kind 捕获代码片段,kind 指定了匹配类型:
| kind | 匹配内容 | 例子 |
|---|---|---|
expr |
表达式 | 1 + 2, foo() |
ident |
标识符 | x, my_func |
ty |
类型 | i32, Vec<String> |
pat |
模式 | Some(x), _ |
stmt |
语句 | let x = 1; |
block |
代码块 | { ... } |
item |
条目 | fn, struct, impl |
tt |
单个 token tree | 任何东西 |
literal |
字面量 | 42, "hello" |
一个带参数的例子:
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("called: {}", stringify!($func_name));
}
};
}
create_function!(foo);
create_function!(bar);
fn main() {
foo(); // 输出: called: foo
bar(); // 输出: called: bar
}
多个匹配分支
宏可以有多个匹配分支,类似 match:
macro_rules! calculate {
(add $a:expr, $b:expr) => { $a + $b };
(mul $a:expr, $b:expr) => { $a * $b };
}
fn main() {
let sum = calculate!(add 1, 2); // 3
let product = calculate!(mul 3, 4); // 12
}
匹配顺序从上到下,先匹配到的优先。
重复模式
重复是宏编程中最有用的特性,语法是 $(...)* 或 $(...)+:
macro_rules! vec_of_strings {
($($element:expr),* $(,)?) => {
{
let mut v = Vec::new();
$(v.push(String::from($element));)*
v
}
};
}
fn main() {
let v = vec_of_strings!["hello", "world", "rust"];
println!("{:?}", v); // ["hello", "world", "rust"]
}
$($element:expr),*:匹配 0 个或多个逗号分隔的表达式$(,)?:可选的尾部逗号$(v.push(...))*:对每个匹配项重复展开
经典示例:自定义 HashMap 宏
标准库没有提供 hashmap! 宏,我们可以自己写一个:
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {{
let mut map = std::collections::HashMap::new();
$(map.insert($key, $value);)*
map
}};
}
fn main() {
let scores = hashmap! {
"Alice" => 90,
"Bob" => 85,
"Carol" => 92,
};
println!("{:?}", scores);
}
宏展开调试
宏写错了很难排查,cargo expand 是救命工具:
# 安装
cargo install cargo-expand
# 展开所有宏
cargo expand
# 展开某个模块
cargo expand module_name
# 展开某个函数(需要 nightly)
cargo expand --tests test_name
cargo expand 会输出宏展开后的完整代码,可以清楚地看到宏实际生成了什么。
另一个技巧是用编译器的宏追踪:
#![feature(trace_macros)]
fn main() {
trace_macros!(true);
vec![1, 2, 3];
trace_macros!(false);
}
编译时会输出宏的展开过程(需要 nightly)。
tt muncher 模式
对于复杂的递归解析,可以用 tt (token tree) muncher 模式——每次消费一部分 token,递归处理剩余部分:
macro_rules! count {
() => { 0usize };
($head:tt $($tail:tt)*) => { 1usize + count!($($tail)*) };
}
fn main() {
let n = count!(a b c d e);
println!("{}", n); // 5
}
这种模式可以实现非常灵活的语法解析,但也容易写出难以理解的代码。
小结
macro_rules! 是 Rust 元编程的基础。对于大多数场景,声明宏就够用了。如果需要更强的能力(比如自动实现 trait),就要用过程宏(derive macro / attribute macro),那是另一个话题了。