内用规则
#[macro_export] macro_rules! foo { (@as_expr $e:expr) => {$e}; ($($tts:tt)*) => { foo!(@as_expr $($tts)*) }; } fn main() { assert_eq!(foo!(42), 42); }
内用规则可用在以下两种情况:
- 将多个宏统一为一个;
- 通过显式命名宏中调用的规则,来简化
TT
“撕咬机” 的读写。
那么为什么将多个宏统一为一个有用呢? 主要原因是:在 2015 版本中,未对宏进行空间命名。这导致一个问题——必须重新导出内部定义的所有宏, 从而污染整个全局宏命名空间;更糟糕的是,宏与其他 crate 的同名宏发生冲突。 简而言之,这很造成很多麻烦。 幸运的是,在 rustc版本 >= 1.30 的情况下(即 2018 版本之后), 这不再是问题了(但是内用规则可以减少不必要声明的宏), 有关宏导出更多信息,请参阅本书 导入/导出宏 。
好了,让我们讨论如何利用“内用规则” (internal rules) 来把多个宏统一为一个, 以及“内用规则”这项技术到底是什么吧。
这个例子有两个宏,一个常见的 as_expr!
宏
和 foo!
宏,后者使用了前者。如果分开写就是下面的形式:
#[macro_export] macro_rules! as_expr { ($e:expr) => {$e} } #[macro_export] macro_rules! foo { ($($tts:tt)*) => { as_expr!($($tts)*) }; } fn main() { assert_eq!(foo!(42), 42); }
这当然不是最好的解决办法,正如前面提到的,因为 as_expr
污染了全局宏命名空间。
在这个特定的例子里,as_expr
只是一个简单的宏,它只会被使用一次,
所以,利用内用规则,把它“嵌入”到 foo
这个宏里面吧!
在 foo
仅有的一条规则前面添加一条新匹配模式(新规则),
这个匹配模式由 as_expr
组成(和命名),然后附加上宏的输入参数 $e:expr
;
在展开里填写这个宏被匹配到时具体的内容。从而得到本章开头的代码:
#[macro_export] macro_rules! foo { (@as_expr $e:expr) => {$e}; ($($tts:tt)*) => { foo!(@as_expr $($tts)*) }; } fn main() { assert_eq!(foo!(42), 42); }
可以看到,没有调用 as_expr
宏,而是递归调用在参数前放置了特殊标记树的 foo!(@as_expr $($tts)*)
。
要是你看得仔细些,你甚至会发现这个模式能好地结合 TT
撕咬机 !
之所以用 @
,是因为在 Rust 1.2 下,该标记尚无任何在前缀位置的用法;
因此,这个语法定义在当时不会与任何东西撞车。
如果你想用别的符号或特有前缀都可以(比如试试 #
、!
),
但 @
的用例已被传播开来,因此,使用它可能更容易帮助读者理解你的代码。
注意:
@
符号很早之前曾作为前缀被用于表示被垃圾回收了的指针, 那时 Rust 还在采用各种记号代表指针类型。而现在的
@
只有一种用法: 将名称绑定至模式中(譬如match
的模式匹配中)。 在这种用法中它是中缀运算符,与我们的上述用例并不冲突。
还有一点要注意,内用规则通常应排在“真正的”规则之前。
这样做可避免 macro_rules!
错把内用规则调用解析成别的东西,比如表达式。
性能建议
内用规则的一个缺点是它们会增加编译时间。
即便最终只有一条规则的宏可以匹配(有效的)宏调用,但编译器必须尝试按顺序匹配所有规则。
如果宏有许多规则,则可能会有许多匹配失败的情况,而使用内部规则会增加此类匹配失败的数量。
此外,@as_expr
方式的标识符使规则变得更长,这略微增加了编译器在匹配时必须做的工作量。
因此,为了获得最佳性能,最好避免使用内部规则。
避免使用它们通常也会使复杂的宏更易于阅读。