在 rust 中,我们一开始就在使用宏,例如 println!, vec!, assert_eq! 等。看起来宏和函数在使用时只是多了一个 !。实际上这些宏都是声明式宏(也叫示例宏或macro_rules!),rust 还支持过程宏,过程宏为我们提供了强大的元编程工具。
声明式宏类似于 match 匹配。它可以将表达式的结果与多个模式进行匹配。一旦匹配成功,那么该模式相关联的代码将被展开。和 match 不同的是,宏里的值是一段 rust 源代码。所有这些都发生在编译期,并没有运行期的性能损耗。下面是一个例子:
// 声明一个add宏
macro_rules! add {
($a: expr, $b: expr) => {
$a + $b
};
}
fn main() {
let a = 10;
let b = 22;
let _res = add!(a, b);
let _res = add!(a+1, b);
let _res = add!(a*2, b+3);
}
我们需要一个类似于 GCC -E 的方式来查看一下预处理阶段之后的代码。cargo-expand 正好提供了相应的功能。使用 cargo 安装 cargo-expand 即可。
cargo install cargo-expand
安装 cargo-expand 之后,可以使用 cargo expand 命令来查看声明式宏是如何被展开的。上面的代码在执行cargo expand之后输出如下所示:
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
let a = 10;
let b = 22;
let _res = a + b;
let _res = a + 1 + b;
let _res = a * 2 + (b + 3);
}
可以看到,每一个 _res 的右边都被展开了,并且如果传入的参数是一个表达式,则会将整个表达式作为一个整体传递给宏。这就是某些地方提到的“Hygienic Macros”(有些地方也翻译为卫生宏,翻译的很抽象)。最后一行代码中传入的b+3被当做了一个整体。如果是在C/C++中,不会自动将表达式作为整体,而是直接进行字符串替换。而 Rust 编译器会自动处理变量名和作用域,确保宏展开后的代码不会引入未预料的变量冲突。下面是一个C/C++中使用宏的例子。
#include
#define ADD(a, b) a + b;
int main() {
int a = 10;
int b = 22;
int _res = ADD(a, b)
_res = ADD(a+1, b)
_res = ADD(a*2, b+3)
}
同样,我们使用 gcc -E main.c 来获取预处理之后的代码。由于展开之后的代码非常得多,我们只放上 main 函数中展开的部分。
int main() {
int a = 10;
int b = 22;
int _res = a + b;
_res = a+1 + b;
_res = a*2 + b+3;
}
可以看到,调用的代码展开之后,并没有将 b+3 作为一个整体来处理,而是简单的进行替换。因此,我们在 C/C++ 中编写宏要特别注意,宏参数在使用的时候必须加上括号。现在我们来修复上面 C/C++ 代码中的宏。
#include
#define ADD(a, b) (a) + (b);
int main() {
int a = 10;
int b = 22;
int _res = ADD(a, b)
_res = ADD(a+1, b)
_res = ADD(a*2, b+3)
}
这样,我们在使用宏的时候,就避免了意外结果的发生。这样展开之后的代码如下所示:
int main() {
int a = 10;
int b = 22;
int _res = (a) + (b);
_res = (a+1) + (b);
_res = (a*2) + (b+3);
}
我们接着来定义我们自己的 my_vec! 宏, 来对声明式宏的相关语法做一个解释。
macro_rules! my_vec {
// 匹配 my_vec![]
() => {
std::vec::Vec::new()
};
// 匹配 my_vec![1,2,3]
($($el:expr), *) => {
// 这段代码需要用{}包裹起来,因为宏需要展开,这样能保证作用域正常,不影响外部。这也是rust的宏是 Hygienic Macros 的体现。
// 而 C/C++ 的宏不强制要求,但是如果遇到代码片段,在 C/C++ 中也应该使用{}包裹起来。
{
let mut v = std::vec::Vec::new();
$(v.push($el);)*
v
}
};
// 匹配 my_vec![1; 3]
($el:expr; $n:expr) => {
std::vec::from_elem($el, $n)
};
}
$
开头的标识符来声明。每个参数都需要提供类型,这里 expr
代表表达式,所以 $el:expr
是说把匹配到的表达式命名为 $el
。$(...),*
告诉编译器可以匹配任意多个以逗号分隔的表达式,然后捕获到的每一个表达式可以用 $el
来访问。由于匹配的时候匹配到一个 $(...)*
(我们可以不管分隔符),在执行的代码块中,我们也要相应地使用 $(...)*
展开。所以这句 $(v.push($el);)*
相当于匹配出多少个 $el
就展开多少句 push 语句。我们来使用一下自定义的 my_vec! 宏
let mut v = my_vec!();
v.push(1);
println!("{:?}", v);
let v = my_vec![1, 2, 3, 4, 5];
println!("{:?}", v);
let v = my_vec!{1; 3};
println!("{:?}", v);
我们在使用宏的时候,可以使用(), [], {},都是可以的。但是一般都是按照约定成俗的方式来使用。例如:vec![1,2,3]
,而不是使用 vec!{1,2,3}
。
这段宏调用,展开以后,如下所示:
let mut v = std::vec::Vec::new();
v.push(1);
{
::std::io::_print(format_args!("{0:?}\n", v));
};
let v = {
let mut v = std::vec::Vec::new();
v.push(1);
v.push(2);
v.push(3);
v.push(4);
v.push(5);
v
};
{
::std::io::_print(format_args!("{0:?}\n", v));
};
let v = std::vec::from_elem(1, 3);
{
::std::io::_print(format_args!("{0:?}\n", v));
};
可以看到,let v = my_vec![1, 2, 3, 4, 5];
被展开为
let v = {
let mut v = std::vec::Vec::new();
v.push(1);
v.push(2);
v.push(3);
v.push(4);
v.push(5);
v
};
它带上了我们在宏定义中的{},另外我们注意到println! 宏也被展开了, 但是并没有完全展开,其中还包含了一个format_args! 宏,我们来看一下,是否和println宏的定义一样。
// println宏的定义
macro_rules! println {
() => {
$crate::print!("\n")
};
($($arg:tt)*) => {{
$crate::io::_print($crate::format_args_nl!($($arg)*));
}};
}
可以看到,println带有参数将会使用 format_args_nl! 宏,但是expand确是 format_args 宏。大概可能是因为文档中说format_args_nl宏是nightly模式下的吧!并没有完全展开是因为该宏是内置宏(rustc_builtin_macro)。
在使用声明宏时,我们需要为参数明确类型,刚才的例子都是使用的expr,其实还可以使用下面这些:
#[...]`` 和
#![…]`` 属性内部的数据。声明式宏还算比较简单。它可以帮助我们解决一些问题。
宏目前的编写无法得到IDE很好的支持,另外一点就是如无必要,就不要编写宏。如果要编写,那么尽量编写声明式宏,而不是过程宏。