Rust 宏
在语法那一页,你学过一条经验法则:带 ! 的是宏,不是普通函数,比如 println!、vec!、format!。这一页终于要讲清楚,这到底是什么意思。
为什么 println! 不能是普通函数
Section titled “为什么 println! 不能是普通函数”像 fn add(a: i32, b: i32) -> i32 这样的普通函数,参数的个数和类型总是固定的。可是调用 println! 时,参数的个数和类型可以千差万别:
fn main() { println!("这里没有参数"); println!("{}", 42); println!("{} 和 {}", "Rust", 2);}输出:
这里没有参数42Rust 和 2没有哪个普通函数的签名能同时接受这几种调用。宏(macro)的工作方式不一样:它在你的代码完全编译之前运行,根据你调用它的方式生成真正的 Rust 代码。println!("{} 和 {}", "Rust", 2) 实际上会展开成一段专门处理这两个参数的代码。每种调用方式,展开出来的代码都不一样。
写一个简单的宏
Section titled “写一个简单的宏”用 macro_rules! 就能定义自己的宏。下面这个宏会求一个数的平方:
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { println!("{}", square!(5));}输出:
25($x:expr) 的意思是“接受一个表达式,把它叫作 $x”。=> { ... } 部分是宏展开后的代码:凡是你写 square!(5) 的地方,编译器都会在真正编译之前把它换成 5 * 5。
接受多个参数的宏
Section titled “接受多个参数的宏”宏可以匹配更复杂的模式,包括一串重复的参数。这是普通函数根本做不到的:
macro_rules! sum { ($($x:expr),*) => { { let mut total = 0; $(total += $x;)* total } };}
fn main() { println!("{}", sum!(1, 2, 3)); println!("{}", sum!(10, 20));}输出:
630$($x:expr),* 的意思是“零个或多个表达式,用逗号隔开”。正是这种模式,让 vec![1, 2, 3] 和 vec![1, 2, 3, 4, 5] 能共用同一个宏定义。
不止一条规则
Section titled “不止一条规则”一个宏可以有好几条规则,用分号隔开。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));}输出:
792.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" |
你真正会在哪里遇到宏
Section titled “你真正会在哪里遇到宏”你会经常用到宏(println!、vec!、format!、#[derive(Debug)]),却几乎不需要自己写。自己写宏是一项比较小众的技能,主要是库的作者用来消除大型代码库里真正重复的样板代码。知道有这么个东西、大致怎么工作就够了,刚入门时不必急着用它。
1. 全部打印出来
Section titled “1. 全部打印出来”写一个宏 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);}输出:
苹果423.5这个宏会展开成三个独立的 println! 调用,每个参数一个。每个值的类型都可以不同,普通函数可处理不了这种情况。
2. 自己的 vec!
Section titled “2. 自己的 vec!”写一个用法和 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
- 宏 — Rust 程序设计语言:
vec!是怎么定义的,还介绍了#[derive]这类过程宏。 macro_rules!— 通过例子学 Rust:关于指示符、重载和重复的简短可运行示例。- Macros By Example — The Rust Reference(英文):
expr、ident、tt等片段类型的完整列表,以及匹配的确切规则。 - The Little Book of Rust Macros(英文):一本免费、友好的深入指南,适合想写更大的宏时阅读。