内用规则

#[macro_export]
macro_rules! foo {
    (@as_expr $e:expr) => {$e};

    ($($tts:tt)*) => {
        foo!(@as_expr $($tts)*)
    };
}

fn main() {
    assert_eq!(foo!(42), 42);
}

内用规则可用在以下两种情况:

  1. 将多个宏统一为一个;
  2. 通过显式命名宏中调用的规则,来简化 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 方式的标识符使规则变得更长,这略微增加了编译器在匹配时必须做的工作量。

因此,为了获得最佳性能,最好避免使用内部规则

避免使用它们通常也会使复杂的宏更易于阅读。