Rust:宏编程入门(macro_rules!)

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),那是另一个话题了。