如果您对宏不熟悉,那么您对 Rust 也不熟悉。
在本指南中,我们将揭秘支撑数十万个 Rust 应用程序的三个宏。它们不是由巫师编写的,而是由我们渴望成为的才华横溢的 Rust 工程师编写的。
这三个宏都是 "声明式 "的,也就是用宏规则(macro_rules! 这些宏构成了 Rust 宏的主体,将标记作为输入,进行模式匹配,并根据匹配结果输出源代码。 程序宏构成了宏家族的其他部分,涉及对标记流的编程操作。
一个简单的 Rust 宏:assert_eq!
let a = 3; let b = 1 + 2; assert_eq!(a, b); assert_eq!(a, b, "we are testing addition with {} and {}", a, b); |
assert_eq!支持两种参数模式。
- 第一种匹配两个表达式,a, b并比较它们的值是否相等。
- 第二种也接受格式字符串和任意数量的参数进行插值。
如果任一匹配模式的相等性检查失败,assert_eq!则会产生panic。
因为我们有两种参数模式,所以你正确地期望宏定义中有两条规则:
#[macro_export] macro_rules! assert_eq { ($left:expr, $right:expr $(,)?) => { ... }; ($left:expr, $right:expr, $($arg:tt)+) => { ... }; } |
- 第一个宏:采用两个表达式 - 等式的左边和右边 - 以及可选的尾随逗号。
- 第二个宏:规则 2 匹配同样的两个表达式,但也匹配与格式字符串及其参数相对应的一个或多个TokenTree 令牌树。
它们的扩展几乎完全相同:
#[macro_export] macro_rules! assert_eq { ($left:expr, $right:expr $(,)?) => { match (&$left, &$right) { (left_val, right_val) => { if !(*left_val == *right_val) { let kind = $crate::panicking::AssertKind::Eq; // ... } } } }; ($left:expr, $right:expr, $($arg:tt)+) => { match (&$left, &$right) { (left_val, right_val) => { if !(*left_val == *right_val) { let kind = $crate::panicking::AssertKind::Eq; // ... } } } }; } |
这两条规则产生的代码都会对左手和右手表达式进行求值,并将输出结果的引用存储在 left_val 和 right_val 3 中。 这样,我们就不必多次求值 $left 或 $right。
请记住,虽然 assert_eq! 例子使用了原始类型,但 $left 和 $right 可能是昂贵的函数调用。
接下来,使用 PartialEq 4 进行简单的相等性检查。 当检查失败时,事情就变得有趣了。
两种规则都将 $crate::panicking::AssertKind::Eq 赋值给变量 kind 5 。
Rust 很聪明,这个宏本地的 kind 变量不会与任何调用 assert_eq! 的名为 kind 的变量发生冲突,这就是所谓的宏卫生(macro hygiene)特性。
但为什么要使用完全限定的模块路径来指定 AssertKind::Eq 呢? 当你使用 #[macro_export] 导出宏时,并不能保证在使用该宏时的作用域中会出现什么。
assert_eq! 的用户不应该被要求自己导入这个实现细节,而完全限定的路径可以避免这一点。
... 更多点击标题