Skip to content

9.1 简介 macro

“宏”(macro)是 Rust 的一个重要特性。Rust 的“宏”(macro)是一种编译器扩展,它的调用方式为some_macro!(...)。 宏调用与普通函数调用的区别可以一眼区分开来,凡是宏调用后面都跟着一个感叹号。 宏也可以通过some_macro![...]some_macro!{ ... }两种语法调用,只要括号能正确匹配即可。 我们在本书一开始就已经使用了“宏”,大家一定记得println!这个宏,它可以用于向标准输出打印字符串。

与 C/C++ 中的宏不一样的是,Rust 中的宏是一种比较安全的“卫生宏”(hygiene)。 首先,Rust 的宏在调用的时候跟函数有明显的语法区别;其次,宏的内部实现和外部调用者处于不同名字空间,它的访问范围严格受限,是通过参数传递进去的,我们不能随意在宏内访问和改变外部的代码。 C/C++中的宏只在预处理阶段起作用,因此只能实现类似文本替换的功能。 而 Rust 中的宏在语法解析之后起作用,因此可以获取更多的上下文信息,而且更加安全。

我们可以把“宏”视为“元编程”的一种方式。它是一种“生成程序的程序”。宏有很多用处。

9.1.1 实现编译阶段检查

比如我们用下面的方式调用println!宏:

rust
fn main() {
    println!("number1 {} number2 {}");
}

编译器会产生一个编译错误“invalid reference to argument 0 (no arguments given)”。 这是因为我们的第一个参数是一个字符串模板,它应该接受两个参数用于内部填充,可是我们在调用的时候,后面没有提供足够的参数,因此出错。 这个功能如果使用普通函数来实现,是不可能在编译阶段实现这样的错误检查功能的。使用宏,我们可以在编译阶段分析这个字符串常量和对应参数,确保它符合约定。 另外一个常见的场景是,利用宏来检查正则表达式的正确性。

9.1.2 实现编译期计算

比如以下代码可以打印出当前源代码的文件名,以及当前代码的行数。这些信息都是纯编译阶段的信息。

rust
fn main() {
    println!("file {} line {} ", file!(), line!());
}

在某些场景下,利用宏来完成一些编译期计算也是一种可行的选择。

9.1.3 实现自动代码生成

有些情况下,许多代码具有同样的“模式”,但是它们不能用现有的语法工具,如“函数”“泛型”“trait”等对其进行合理抽象。 如果这样的 boilerplate 代码数量很多,实际上意味着代码违反了 “Don’t Repeat Yourself” 原则,那么我们可以用“宏”来精简代码,消除重复。

比如,在标准库中就有许多类似的用法。在core/ops.rs代码中,内置类型对各种运算符 trait 的支持就使用了宏。

rust
add_impl! { usize u8 u16 u32 u64 isize i8 i16 i32 i64 f32 f64 }

这是各个内置类型实现std::ops::Add这个 trait 的办法。因为这些代码非常相似,所以可以将它们提取到一个“宏”里面,以避免无聊的重复。

9.1.4 实现语法扩展

某些情况下,我们可以使用宏来设计比较方便的“语法糖”,而不必使用编译器内部硬编码来实现。比如初始化一个动态数组,我们可以使用方便的vec!宏:

rust
let v = vec![1, 2, 3, 4, 5];

简洁、直观、明了,而且不是编译器内部的“黑魔法”。我们可以充分发挥自己的想象力,通过自定义宏来增加语言的表达能力,甚至自定义 DSL(Domain Specific Language)。

Released under the MIT License