跳转到内容

Rust 宏

在语法那一页,你学过一条经验法则:带 ! 的是宏,不是普通函数,比如 println!、vec!、format!。这一页终于要讲清楚,这到底是什么意思。

像 fn add(a: i32, b: i32) -> i32 这样的普通函数,参数的个数和类型总是固定的。可是调用 println! 时,参数的个数和类型可以千差万别:

fn main() {
println!("这里没有参数");
println!("{}", 42);
println!("{} 和 {}", "Rust", 2);
}

输出:

这里没有参数
42
Rust 和 2
▶ 动手试一试

没有哪个普通函数的签名能同时接受这几种调用。宏(macro)的工作方式不一样:它在你的代码完全编译之前运行,根据你调用它的方式生成真正的 Rust 代码。println!("{} 和 {}", "Rust", 2) 实际上会展开成一段专门处理这两个参数的代码。每种调用方式,展开出来的代码都不一样。

用 macro_rules! 就能定义自己的宏。下面这个宏会求一个数的平方:

macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(5));
}

输出:

25
▶ 动手试一试

($x:expr) 的意思是“接受一个表达式,把它叫作 $x”。=> { ... } 部分是宏展开后的代码:凡是你写 square!(5) 的地方,编译器都会在真正编译之前把它换成 5 * 5。

宏可以匹配更复杂的模式,包括一串重复的参数。这是普通函数根本做不到的:

macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
fn main() {
println!("{}", sum!(1, 2, 3));
println!("{}", sum!(10, 20));
}

输出:

6
30
▶ 动手试一试

$($x:expr),* 的意思是“零个或多个表达式,用逗号隔开”。正是这种模式,让 vec![1, 2, 3] 和 vec![1, 2, 3, 4, 5] 能共用同一个宏定义。

一个宏可以有好几条规则,用分号隔开。Rust 会从上往下逐条尝试,用第一条匹配上的规则。一条规则里甚至可以再次调用这个宏。下面这个 max! 可以接受任意个参数:

macro_rules! max {
// 规则 1:只有一个值。最大的就是它自己。
($x:expr) => {
$x
};
// 规则 2:一个值,后面还跟着一个或多个值。
($x:expr, $($rest:expr),+) => {
{
let first = $x;
let biggest_of_rest = max!($($rest),+);
if first > biggest_of_rest { first } else { biggest_of_rest }
}
};
}
fn main() {
println!("{}", max!(7));
println!("{}", max!(3, 9, 4));
println!("{}", max!(2.5, 1.0));
}

输出:

7
9
2.5
▶ 动手试一试

对于 max!(3, 9, 4),规则 1 匹配不上(值不止一个),于是用规则 2。它先把 3 放在一边,展开 max!(9, 4),而这一步又会展开 max!(4)。max!(4) 匹配规则 1,到此为止。顺序很重要:Rust 总是先试规则 1。

这里有两个新的小语法:

  • + 表示一个或多个,而 *(sum! 里用过)表示零个或多个。规则 2 要求逗号后面至少有一个值,所以里面那次 max!(...) 调用总有东西可处理。
  • max! 对整数和浮点数都管用,因为宏只是把 first > biggest_of_rest 这段代码原样贴进去。换成函数的话,就得用泛型才能做到。

宏能接受的不只是 expr。在真实的宏里,你还会见到这几种:

片段类型 匹配什么 例子
expr 任意表达式 2 + 3、name.len()
ident 一个名字 count、Point
ty 一个类型 i32、Vec<String>
literal 一个字面量 42、"hi"

你会经常用到宏(println!、vec!、format!、#[derive(Debug)]),却几乎不需要自己写。自己写宏是一项比较小众的技能,主要是库的作者用来消除大型代码库里真正重复的样板代码。知道有这么个东西、大致怎么工作就够了,刚入门时不必急着用它。

写一个宏 print_all!,接受任意个用逗号隔开的值,并把每个值单独打印一行。print_all!("苹果", 42, 3.5) 应该打印三行。提示:像 sum! 那样匹配 $($x:expr),*,再用 $( ... )* 为每个值重复一次 println!。

查看答案
macro_rules! print_all {
($($x:expr),*) => {
$(println!("{}", $x);)*
};
}
fn main() {
print_all!("苹果", 42, 3.5);
}

输出:

苹果
42
3.5
▶ 动手试一试

这个宏会展开成三个独立的 println! 调用,每个参数一个。每个值的类型都可以不同,普通函数可处理不了这种情况。

写一个用法和 vec! 一样的 my_vec!。my_vec![] 得到一个空的 Vec,my_vec![1, 2, 3] 得到装着这几个元素的 Vec。提示:用两条规则。第一条什么都不匹配,写成 ()。第二条匹配 $($x:expr),+,先创建 Vec::new(),再把每个 $x push 进去。

查看答案
macro_rules! my_vec {
() => {
Vec::new()
};
($($x:expr),+) => {
{
let mut v = Vec::new();
$(v.push($x);)+
v
}
};
}
fn main() {
let empty: Vec<i32> = my_vec![];
let numbers = my_vec![1, 2, 3];
println!("{:?} {:?}", empty, numbers);
}

输出:

[] [1, 2, 3]
▶ 动手试一试

任何宏都可以用 ()、[] 或 {} 来调用。像 vec! 这种表示列表的宏,大家习惯用方括号,但写成 my_vec!(1, 2, 3) 也一样能用。空的 Vec 需要写上 : Vec<i32> 这个类型,因为没有元素时,Rust 没法知道它要装什么。

  • 宏会在编译之前展开成真正的代码,所以 println! 每次调用都能接受不同个数、不同类型的参数。
  • 名字后面带 !,就说明你调用的是宏,而不是函数。
  • 用 macro_rules! 可以定义自己的宏,匹配单个表达式($x:expr)或一串重复的参数($($x:expr),*)这样的模式。
  • 一个宏可以有好几条规则。Rust 从上往下逐条尝试,规则里还可以再次调用这个宏。
  • 自己写宏是小众技能。用好标准库里现成的宏,几乎就能满足你的所有需要。

小测验

1. 为什么 println! 不能写成普通函数?

2. 在 macro_rules! 的模式里,$x:expr 是什么意思?

3. 宏在什么时候展开成真正的代码?

得分: 0 / 3