首页 / Rust 入门教程 / 宏基础

Rust 入门教程

宏基础

本教程共 78 篇 · 第 74 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

RustRust 入门教程macro_rules声明式宏vec!

本节目标:认识 Rust 的声明式宏 macro_rules!,理解它和函数的本质区别,能以简化版 vec! 为例看懂 $( $x:expr ),* 这类模式语法,并知道宏是在编译期展开、没有运行时开销。

你从写第一行 Rust 起就在用宏:println!vec!assert_eq!,它们名字后面都带着一个 !。这一章我们搞清楚宏到底是什么,以及怎么自己写一个声明式宏。

1-1 宏和函数有什么不同

宏和函数长得很像(就多一个 !),但本质不同。宏是元编程:用一段代码生成另一段代码。它在编译器真正解释代码之前就”展开”成普通代码,所以宏没有运行时的性能损耗。

宏相比函数有三个独门绝技:

第一,可变参数。函数签名固定:声明两个参数就必须传两个。而宏可以接收任意数量的参数,比如 println!("hello")println!("hello {}", name) 都能用同一个 println!

第二,能为指定类型实现 trait。宏展开发生在编译期,所以它能先展开成”为某类型实现某 trait”的代码,再被编译。函数做不到——函数到运行时才调用,而 trait 要在编译期就实现好。

第三,处理不同代码模式。宏可以针对不同结构的输入,展开成不同的代码,像个编译期的”分发器”。

代价是:宏难写、难读、难调试。所以原则是——能用函数就别用宏,真需要元编程时才上。

1-2 声明式宏像 match

Rust 里宏分两大类:声明式宏 macro_rules! 和三种过程宏(下一章讲)。声明式宏用得最广,它的写法像一个 match:把一个值(这里是”一段源代码”)和多个模式匹配,匹配上就展开成对应的代码。

下面我们照着标准库 vec! 的思路,写一个简化版:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

#[macro_export] 把宏导出,别的包 use 后就能用。宏的名字是 vec(不是 vec!! 只在调用时出现)。结构像 match:一个分支,模式在 => 左边,展开代码在右边。

1-3 拆解模式 $( $x:expr ),*

这个模式看着吓人,拆开就清楚了:

  • 最外层的 ( ) 把整个宏模式包起来。
  • $x:expr 是一个片段匹配器expr 表示”匹配任意 Rust 表达式”,$x 是给匹配到的东西起的名字,后面展开时用。
  • $( ... ) 外层的 $() 表示”把里面的模式捕获下来,用于后面展开”。
  • 紧跟的逗号 , 表示这些被捕获的部分用逗号分隔。
  • 最后的 * 表示”前面的模式可以出现零次或任意多次”(类似正则的 *)。

所以 $( $x:expr ),* 整体意思是:匹配一串用逗号分隔、数量任意的表达式。当你写 vec![1, 2, 3]$x 会依次匹配到 123

1-4 展开成什么

右边的代码里,$( temp_vec.push($x); )* 表示:根据前面匹配的次数,把 temp_vec.push($x); 这个模板复制相应的份数。于是 vec![1, 2, 3] 会被展开成:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

最后 temp_vec 作为块表达式的值返回。整个展开在编译期完成,运行期就是普普通通的 Vec 操作,没有额外开销。

Note

想看宏展开后的真实代码,可以装 cargo-expandcargo install cargo-expand),然后 cargo expand 查看。调试宏时非常有用。

1-5 一个小坑:尾随逗号

上面简化版有个细节:vec![1, 2, 3,] 会报错,因为模式 $( $x:expr ),* 不允许最后多一个逗号。标准库为了支持尾随逗号,用的是更精细的写法:

($($x:expr),+ $(,)?) => ( ... );

+ 表示”至少出现一次”,$(,)? 表示”末尾的逗号可有可无”。这就是真实 vec! 能写 vec![1, 2, 3,] 的原因。可见宏的细节可以很微妙,所以别轻易造轮子。

1-6 2024 Edition 的小变化

我们全书以 2024 Edition 为基线。对 macro_rules! 来说,日常用法不变,但有一个细节要注意(来源:Rust 1.85.0 暨 2024 Edition 发布说明):

  • expr 片段匹配器现在也能匹配 const 块和 _ 表达式。在 2021 Edition 里,某些情况下 expr 匹配不到这些。绝大多数代码不受影响,只有当你写特别复杂的宏、依赖 expr 的旧匹配边界时,才可能觉察到差异。

另外,2024 Edition 把 missing_fragment_specifier 这个 lint 提升为了硬错误:宏里的元变量(如 $x)必须带片段说明符(exprty 等),不能裸写。这是好事,强迫宏写得更明确。

Tip

macro_rules! 有已知的设计问题,Rust 官方计划未来用一种新的声明式宏取代它,届时 macro_rules 会进入 deprecated 状态。但眼下它仍是声明宏的主流写法,学会了不亏。多数 Rust 开发者是宏的”使用者”而非”编写者”,理解到这一章的程度足矣。

1-7 宏的调试与常见坑

写宏最难受的是报错信息绕。宏展开发生在编译早期,所以宏里的错误常常显示为”展开后的代码”在哪一行出错,而不是你写的宏定义那行。定位时记住:报错指向的是展开产物,要从产物反推回你的宏模式哪里匹配错了,而不是盯着宏定义发呆。

另一个坑是宏的”卫生性”(hygiene):宏里定义的变量名,默认不会和外层代码的同名变量冲突,这是好事;但如果你硬要在宏里引用外层的一个变量,得用 $(...) 把名字传进来,而不能凭空写个同名标识符指望它连上。新手常在 vec! 式宏里想引用外部 temp_vec 却失败,就是这个原因——宏有自己独立的命名空间。

调试宏强烈推荐 cargo expand:它能把宏展开后的真实 Rust 代码打印出来,你一眼就能看出展开对不对。装法:cargo install cargo-expand,然后 cargo expand。有了这把”照妖镜”,宏不再是黑盒。

Warning

宏报错难读,是它”难维护”名声的来源之一。再次强调:能用函数/泛型解决的,绝不动宏。宏只留给真正需要元编程的地方。

1-8 小结

声明式宏 macro_rules! 是”编译期 match”:用模式匹配一段源代码,再展开成对应代码,零运行时开销。核心语法 $( $x:expr ),* 表示”逗号分隔、数量任意的表达式”。宏强大但难维护,原则是能写函数就别写宏。下一章我们看更灵活、也更复杂的过程宏。