Rust 提供了一个强大的宏系统,可进行元编程(metaprogramming)。你已经在前面的章节中看到,宏看起来和函数很像,只不过名称末尾有一个感叹号 ! 。宏并不产生函数调用,而是展开成源码,并和程序的其余部分一起被编译。Rust 又有一点和 C 以及其他语言都不同,那就是 Rust 的宏会展开为抽象语法树(AST,abstract syntax tree),而不是像字符串预处理那样直接替换成代码,这样就不会产生无法预料的优先权错误。
宏是通过 macro_rules! 宏来创建的。
// 第17章 使用 macro_rules! 来创建宏
// 这是一个简单的宏,名为 `say_hello`。
macro_rules! say_hello {
// `()` 表示此宏不接受任何参数。
() => {
// 此宏将会展开成这个代码块里面的内容。
println!("Hello!");
}
}
fn main() {
// 这个调用将会展开成 `println("Hello");`!
say_hello!();
println!("Hello Rust");
}
// rustc main.rs
// ./main
编译运行:
PS F:\rustproject\rustbyexample\chapter17\example17_1> rustc main.rs
PS F:\rustproject\rustbyexample\chapter17\example17_1> ./main
Hello!
Hello Rust
PS F:\rustproject\rustbyexample\chapter17\example17_1>

为什么宏是有用的?
- 不写重复代码(DRY,Don't repeat yourself.)。很多时候你需要在一些地方针对不同 的类型实现类似的功能,这时常常可以使用宏来避免重复代码(稍后详述)。
- 领域专用语言(DSL,domain-specific language)。宏允许你为特定的目的创造特定的 语法(稍后详述)。
- 可变接口(variadic interface)。有时你需要能够接受不定数目参数的接口,比如
println!,根据格式化字符串的不同,它需要接受任意多的参数(稍后详述)。
17.1 语法
在下面的小节中,我们将展示如何在 Rust 中定义宏。基本的概念有三个:
17.1.1 指示符
宏的参数使用一个美元符号 $ 作为前缀,并使用一个指示符(designator)来注明类型:
// 17.1.1 指示符
macro_rules! create_function {
// 此宏接受一个 `ident` 指示符表示的参数,并创建一个名为 `$func_name` 的函数。
// `ident` 指示符用于变量名或函数名
($func_name: ident) => (
fn $func_name() {
// `stringify!` 宏把 `ident` 转换成字符串。
println!("You called {:?}()", stringify!($func_name))
}
)
}
// 借助上述宏来创建名为 `foo` 和 `bar` 的函数。
create_function!(foo);
create_function!(bar);
macro_rules! print_result {
// 此宏接受一个 `expr` 类型的表达式,并将它作为字符串,连同其结果一起
// 打印出来。
// `expr` 指示符表示表达式。
($expression: expr) => (
// `stringify!` 把表达式*原样*转换成一个字符串。
println!("{:?} = {:?}", stringify!($expression), $expression)
)
}
fn main() {
foo();
bar();
print_result!(1u32 + 1);
// 回想一下,代码块也是表达式!
print_result!({
let x = 1u32;
x * x + 2 * x - 1
});
println!("Hello Rust");
}
// rustc main.rs
// ./main
编译运行:
PS F:\rustproject\rustbyexample\chapter17\example17_2> rustc main.rs
PS F:\rustproject\rustbyexample\chapter17\example17_2> ./main
You called "foo"()
You called "bar"()
"1u32 + 1" = 2
"{ let x = 1u32; x * x + 2 * x - 1 }" = 2
Hello Rust
PS F:\rustproject\rustbyexample\chapter17\example17_2>

这里列出全部指示符:
blockexpr用于表达式ident用于变量名或函数名itemliteral用于字面常量pat(模式 pattern)pathstmt(语句 statement)tt(标记树 token tree)ty(类型 type)vis(可见性描述符)
完整列表详见 Rust Reference。
17.1.2 重载
宏可以重载,从而接受不同的参数组合。在这方面,macro_rules! 的作用类似于匹配(match)代码块:
// 17.1.2 重载
// 根据你调用它的方式,`test!` 将以不同的方式来比较 `$left` 和 `$right`。
macro_rules! test {
// 参数不需要使用逗号隔开。
// 参数可以任意组合!
($left: expr; and $right:expr) => (
println!("{:?} and {:?} is {:?}",
stringify!($left),
stringify!($right),
$left && $right)
);
// ^ 每个分支都必须以分号结束。
($left: expr; or $right: expr) => (
println!("{:?} or {:?} is {:?}",
stringify!($left),
stringify!($right),
$left || $right)
);
}
fn main() {
test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
test!(true; or false);
println!("Hello Rust");
}
// rustc main.rs
// ./main
编译运行:
PS F:\rustproject\rustbyexample\chapter17\example17_3> rustc main.rs
PS F:\rustproject\rustbyexample\chapter17\example17_3> ./main
"1i32 + 1 == 2i32" and "2i32 * 2 == 4i32" is true
"true" or "false" is true
Hello Rust
PS F:\rustproject\rustbyexample\chapter17\example17_3>

17.1.3 重复
宏在参数列表中可以使用 + 来表示一个参数可能出现一次或多次,使用 * 来表示该参数可能出现零次或多次。
在下面例子中,把模式这样: $(...),+ 包围起来,就可以匹配一个或多个用逗号隔开的表达式。另外注意到,宏定义的最后一个分支可以不用分号作为结束。
// 17.1.3 重复
// `min!` 将求出任意数量的参数的最小值。
macro_rules! find_min{
// 基本情形:
($x: expr) => ($x);
// `$x` 后面跟着至少一个 `$y,`
($x: expr, $($y: expr), +) => (
// 对 `$x` 后面的 `$y` 们调用 `find_min!`
std::cmp::min($x, find_min!($($y), +))
)
}
fn main() {
println!("{}", find_min!(1u32));
println!("{}", find_min!(1u32 + 2, 2u32));
println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
println!("Hello Rust");
}
// rustc main.rs
// ./main
编译运行:
PS F:\rustproject\rustbyexample\chapter17\example17_4> rustc main.rs
PS F:\rustproject\rustbyexample\chapter17\example17_4> ./main
1
2
4
Hello Rust
PS F:\rustproject\rustbyexample\chapter17\example17_4>

17.2 DRY(不写重复代码)
通过提取函数或测试集的公共部分,宏可以让你写出 DRY 的代码(DRY 是 Don't Repeat Yourself 的缩写,意思为 "不要写重复代码")。这里给出一个例子,对 Vec<T> 实现并测试了关于 +=、*= 和 -= 等运算符。
// 17.2 DRY(不写重复代码)
use std::ops::{Add, Mul, Sub};
macro_rules! assert_equal_len {
// `tt`(token tree,标记树)指示符表示运算符和标记。
($a: ident, $b: ident, $func: ident, $op: tt) => (
assert!($a.len() == $b.len(),
"{:?}: dimension mismatch: {:?} {:?} {:?}",
stringify!($func),
($a.len(),),
stringify!($op),
($b.len(),));
)
}
macro_rules! op {
($func: ident, $bound: ident, $op: tt, $method: ident) => (
fn $func<T: $bound<T, Output = T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) {
assert_equal_len!(xs, ys, $func, $op);
for (x, y) in xs.iter_mut().zip(ys.iter()) {
*x = $bound::$method(*x, *y);
// *x = x.$method(*y);
}
}
)
}
// 实现 `add_assign`、`mul_assign` 和 `sub_assign` 等函数。
op!(add_assign, Add, +=, add);
op!(mul_assign, Mul, *=, mul);
op!(sub_assign, Sub, -=, sub);
mod test {
use std::iter;
macro_rules! test {
($func: ident, $x: expr, $y: expr, $z: expr) => {
#[test]
fn $func() {
for size in 0usize..10 {
let mut x: Vec<_> = iter::repeat($x).take(size).collect();
let y: Vec<_> = iter::repeat($y).take(size).collect();
let z: Vec<_> = iter::repeat($z).take(size).collect();
super::$func(&mut x, &y);
assert_eq!(x, z);
}
}
}
}
// 测试 `add_assign`、`mul_assign` 和 `sub_assign`
test!(add_assign, 1u32, 2u32, 3u32);
test!(mul_assign, 2u32, 3u32, 6u32);
test!(sub_assign, 3u32, 2u32, 1u32);
}
// rustc --test main.rs
// ./main
编译运行:
PS F:\rustproject\rustbyexample\chapter17\example17_5> rustc --test main.rs
PS F:\rustproject\rustbyexample\chapter17\example17_5> ./main
running 3 tests
test test::mul_assign ... ok
test test::add_assign ... ok
test test::sub_assign ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
PS F:\rustproject\rustbyexample\chapter17\example17_5>

17.3 DSL(领域专用语言)
DSL 是 Rust 的宏中集成的微型 "语言"。这种语言是完全合法的,因为宏系统会把它转换成普通的 Rust 语法树,它只不过看起来像是另一种语言而已。这就允许你为一些特定功能创造一套简洁直观的语法(当然是有限制的)。
比如说我想要定义一套小的计算器 API,可以传给它表达式,它会把结果打印到控制台上。
// 17.3 DSL(领域专用语言)
macro_rules! calculate {
(eval $e: expr) => {{
{
let val: usize = $e; // 强制类型为整型
println!("{} = {}", stringify!{$e}, val);
}
}};
}
fn main() {
calculate! {
eval 1 + 2 // 看到了吧,`eval` 可并不是 Rust 的关键字!
}
calculate! {
eval (1 + 2) * (3 / 4)
}
println!("Hello Rust");
}
// rustc main.rs
// ./main
输出:
PS F:\rustproject\rustbyexample\chapter17\example17_6> rustc main.rs
PS F:\rustproject\rustbyexample\chapter17\example17_6> ./main
1 + 2 = 3
(1 + 2) * (3 / 4) = 0
Hello Rust
PS F:\rustproject\rustbyexample\chapter17\example17_6>

这个例子非常简单,但是已经有很多利用宏开发的复杂接口了,比如 lazy_static 和 clap。
17.4 可变参数接口
可变参数接口可以接受任意数目的参数。比如说 println 就可以,其参数的数目是由格式化字符串指定的。
我们可以把之前的 calculate! 宏改写成可变参数接口:
vb
``// 17.4 可变参数接口
macro_rules! calculate {
// 单个 `eval` 的模式
(eval $e: expr) => {````{
{
let val: usize = $e; // Force types to be integers
println!("{} = {}", stringify!{$e}, val);
}
}};
// 递归地拆解多重的 `eval`
(eval $e: expr, $(eval $es: expr), +) => {````{
calculate! { eval $e }
calculate! { $(eval $es), +}
}};
}
fn main() {
calculate! { // 妈妈快看,可变参数的 `calculate!`!
eval 1 + 2,
eval 3 + 4,
eval (2 * 3) + 1
}
println!("Hello Rust");
}
// rustc main.rs
// ./main``
输出:
PS F:\rustproject\rustbyexample\chapter17\example17_7> rustc main.rs
PS F:\rustproject\rustbyexample\chapter17\example17_7> ./main
1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7
Hello Rust
PS F:\rustproject\rustbyexample\chapter17\example17_7>
