ARTICLE · 1348188
Rust 里的宏,其实就是“代码模板”
上一篇我们讲了怎么发布 crate。
这一篇讲 声明宏 macro_rules!。
很多人一听“宏”就头大。其实用一句话就能理解:
宏就是代码模板。
你写一个模板,告诉编译器“遇到这个写法,就按模板替换”。编译器在编译的时候帮你把模板填好,生成真正的代码。
就这么简单。
一、宏和函数,差在哪?
先看函数:
rust
fn add(a: i32, b: i32) -> i32 {a + b}
函数的参数是值。调用 add(1, 2),1 和 2 已经算好了,传进去。
宏不一样。宏接收的是代码本身。
rust
println!("hello");println! 拿到的不是“hello 这个值”,而是 "hello" 这段代码。然后它把这段代码展开成一大段打印逻辑。
打个比方:
* 函数像自动售货机:你投币,它出货。币和货都是“东西”。
* 宏像施工图纸:你给图纸,它照着盖房子。图纸不是房子,是“怎么盖”的描述。
所以:
函数处理值,宏处理代码。
二、最简单的宏
rust
macro_rules! say_hello {() => {println!("hello");};}fn main() {say_hello!();}
一行行看:
* macro_rules!:我要定义一个宏。
* say_hello:宏的名字。
* () => { ... }:规则。左边是“长什么样”,右边是“替换成什么”。
* say_hello!():调用宏。名字后面必须带 !。
编译的时候,say_hello!() 会被原地替换成 println!("hello")。
运行输出:
text
hello就这么回事。宏就是“看到这个,换成那个”。
三、带参数的宏
rust
macro_rules! square {($x:expr) => {$x * $x};}fn main() {let n = square!(5);println!("{}", n); // 25}
拆开看:
* $x:挖一个坑,名字叫 x。
* :expr:这个坑里只能填表达式。
* $x * $x:把坑里的东西填进来,算两次。
调用 square!(5),编译器看到 5 是表达式,填进坑里,展开成:
rust
5 * 5expr 的意思是“这里必须是表达式”。填别的东西,编译报错。
四、坑的类型:常用说明符
挖坑的时候要说明“这个坑能填什么”。
说明符 | 能填什么 | 例子 |
expr | 表达式 | 1 + 2、foo() |
ident | 名字 | name、foo |
ty | 类型 | i32、Vec<String> |
pat | 模式 | Some(x)、_ |
block | 代码块 | { ... } |
literal | 字面量 | 42、"hello" |
tt | 几乎任何东西 | 万能坑,但约束最少 |
最常用的是 expr、ident、ty。
tt 是万能坑,什么都能填。但因为它什么都不限制,出错时提示很模糊。
能用具体的就用具体的,别偷懒用 tt。
五、多条规则:从上往下试
一个宏可以写多条规则,像 match 一样,从上往下匹配。
rust
macro_rules! my_vec {() => {Vec::new()};($($x:expr),*) => {vec![$($x),*]};}fn main() {let empty: Vec<i32> = my_vec![];let nums = my_vec![1, 2, 3];println!("{:?}", empty); // []println!("{:?}", nums); // [1, 2, 3]}
第一条规则:什么都没填,返回空 Vec。
第二条规则:填了东西,用 vec! 包起来。
编译器先试第一条,不匹配再试第二条。
六、可变参数:$() 和 *
这是宏最厉害的地方:参数数量可以不固定。
rust
macro_rules! my_vec {($($x:expr),*) => {{let mut v = Vec::new();$(v.push($x);)*v}};}
这段看起来吓人,其实就两个新东西:
$($x:expr),*
意思是:零个或多个表达式,用逗号隔开。
* $( 和 ) 把重复的部分包起来。
* $x:expr 是每次重复要抓的东西。
* , 是分隔符。
* * 表示“零个或多个”。
最后组合起来解读:就是建立一条传送带 $(),上面放一台抓取机(抓表达式 $x)和一个逗号 ,。然后按下无限循环开关 *。只要用户输入的东西符合‘表达式+逗号’的规律,你就一直抓、一直抓,直到抓完为止。
$(v.push($x);)*
意思是:对每个抓到的 $x,生成一条 v.push(...)。
同样是 $() 包起来,后面 * 表示重复。
所以 my_vec![1, 2, 3] 展开成:
rust
let mut v = Vec::new();v.push(1);v.push(2);v.push(3);v
*、+、? 的区别:
1. *:零个或多个。
2. +:一个或多个。
3. ?:零个或一个。
my_vec![] 用 * 能匹配,用 + 就报错,因为一个都没有。
七、一个能用的例子:max!
rust
macro_rules! max {($x:expr) => {$x};($x:expr, $($rest:expr),+) => {{let a = $x;let b = max!($($rest),+);if a > b { a } else { b }}};}fn main() {println!("{}", max!(1)); // 1println!("{}", max!(3, 1, 4, 1, 5)); // 5}
逻辑:
* 只有一个参数:直接返回。
* 多个参数:拿第一个,剩下的一路递归比较。
宏可以调用自己,这是处理不定数量参数的常用办法。
八、卫生性:宏里的变量不会串
Rust 宏有个好特性:宏里定义的变量,不会和外面的变量打架。
rust
macro_rules! bad {() => {let x = 1;};}fn main() {let x = 2;bad!();println!("{}", x); // 2}
宏里有个 x,外面也有个 x。但宏里的 x 是独立的,不会把外面的 x 改掉,这叫卫生性。
C 语言的宏是纯文本替换,很容易出现“我明明没改这个变量,怎么值变了”的问题。Rust 从根上避免了。
九、导出宏:让别人也能用
想让别的 crate 用你的宏,加 #[macro_export]:
rust
#[macro_export]macro_rules! my_macro {() => {println!("hello");};}
加了之后,别人可以:
rust
use my_crate::my_macro;my_macro!();
关键坑:用 $crate
宏里如果要引用自己 crate 里的东西,不能写 crate::,要写 $crate::。
rust
#[macro_export]macro_rules! create_user {($name:expr) => {$crate::User {name: $name.to_string(),}};}
为什么?
* crate:: 在用的人的 crate 里找,可能找不到。
* $crate:: 在定义宏的人的 crate 里找,永远对。
这是写导出宏时最容易踩的坑,记住就行。
十、常见坑
1. 忘了 $
rust
macro_rules! bad {(x:expr) => { x }; // 错,应该是 $x}
挖坑和填坑都要带 $。
2. * 和 + 搞混
rust
($($x:expr),*) // 允许零个($($x:expr),+) // 至少一个
my_vec![] 只能用 *。
3. 展开后少分号
rust
$(v.push($x);)* // push 是语句,必须带分号4. 导出宏用了 crate::
改成 $crate::。
5. 用了 tt 导致报错看不懂
tt 什么都不限制,出错时编译器也说不清哪里错了。
能用 expr 就别用 tt。
6. 忘了宏名后面加 !
rust
say_hello(); // 错,这是函数调用say_hello!(); // 对
十一、小练习
1. 最简单的宏
rust
macro_rules! hello {() => {println!("hello");};}fn main() {hello!();}
2. 带参数的宏
rust
macro_rules! square {($x:expr) => {$x * $x};}fnmain() {println!("{}", square!(5));}
3. 可变参数
rust
macro_rules! my_vec {($($x:expr),*) => {{let mut v = Vec::new();$(v.push($x);)*v}};}fn main() {let v = my_vec![1, 2, 3];println!("{:?}", v);}
4. 多条规则
rust
macro_rules! describe {() => { println!("空"); };($x:expr) => { println!("一个: {}", $x); };($x:expr, $($rest:expr),+) => {println!("多个,第一个: {}", $x);};}fn main() {describe!();describe!(1);describe!(1, 2, 3);}
5. 递归计数
rust
macro_rules! count {() => { 0 };($x:expr) => { 1 };($x:expr, $($rest:expr),+) => {1 + count!($($rest),+)};}fn main() {println!("{}", count!()); // 0println!("{}", count!(1, 2, 3)); // 3}
十二、小结
今天只记住声明宏这几件事:
* 宏就是代码模板,编译期替换。
* macro_rules! 定义宏,名字后面调用要加 !。
* 规则左边是模式,右边是展开后的代码。
* $x:expr 挖坑,说明能填什么。
* 常用说明符:expr、ident、ty、tt。
* 多条规则从上往下匹配。
* $($x:expr),* 匹配零个或多个,用逗号分隔。
* $(...)* 对每个匹配项重复生成代码。
* * 零个或多个,+ 至少一个,? 零个或一个。
* 宏可以递归调用自己。
* 卫生性保证宏里的变量不污染外部。
* #[macro_export] 导出,导出宏里用 $crate::。
一句话总结:
宏就是编译期的代码模板。macro_rules! 定义,$x:expr 挖坑,$() 处理重复,多条规则从上往下试,递归处理不定数量参数。导出用 #[macro_export],引用自己用 $crate::。
下一篇,我们讲 宏:过程宏入门。
我们下篇见。















































































