macro_rules!

有了这些知识,我们终于可以引入macro_rules! 了。 如前所述,macro_rules!本身就是一个语法扩展,也就是说它并不是Rust语法的一部分。它的形式如下:

  1. macro_rules! $name {
  2. $rule0 ;
  3. $rule1 ;
  4. // …
  5. $ruleN ;
  6. }

至少得有一条规则,最后一条规则后面的分号可被省略。

每条“规则”(rule)都形如:

  1. ($pattern) => {$expansion}

实际上,分组符号可以是任意一种,选用这种(pattern外小括号、expansion外花括号)只是出于传统。

如果你好奇的话,macro_rules!的调用将被展开为空。至少可以说,在AST中它被展开为空。它所影响的是编译器内部的结构,以将该宏注册进系统中去。因此,技术上讲你可以在任何一个空展开合法的位置插入macro_rules!的调用。

匹配

当一个宏被调用时,对应的macro_rules解释器将一一依序检查规则。对每条规则,它都将尝试将输入标记树的内容与该规则的pattern进行匹配。某个模式必须与输入完全匹配才能被选中为匹配项。

如果输入与某个模式相匹配,则该调用项将被相应的expansion内容所取代;否则,将尝试匹配下条规则。如果所有规则均匹配失败,则宏展开会失败并报错。

最简单的例子是空模式:

  1. macro_rules! four {
  2. () => {1 + 3};
  3. }

它将且仅将匹配到空的输入(即four!()four![]four!{})。

注意调用所用的分组标记并不需要匹配定义时采用的分组标记。也就是说,你可以通过four![]调用上述宏,此调用仍将被视作匹配。只有调用时的输入内容才会被纳入匹配考量范围。

模式中也可以包含字面标记树。这些标记树必须被完全匹配。将整个对应标记树在相应位置写下即可。比如,为匹配标记序列4 fn ['spang "whammo"] @_@,我们可以使用:

  1. macro_rules! gibberish {
  2. (4 fn ['spang "whammo"] @_@) => {...};
  3. }

捕获

宏模式中还可以包含捕获。这允许输入匹配在某种通用语法基础上进行,并使得结果被捕获进某个变量中。此变量可在输出中被替换使用。

捕获由$符号紧跟一个标识符(identifier)紧跟一个冒号(:)紧跟捕获种类组成。捕获种类须是如下之一:

  • item: 条目,比如函数、结构体、模组等。
  • block: 区块(即由花括号包起的一些语句加上/或是一项表达式)。
  • stmt: 语句
  • pat: 模式
  • expr: 表达式
  • ty: 类型
  • ident: 标识符
  • path: 路径 (例如 foo, ::std::mem::replace, transmute::<_, int>, …)
  • meta: 元条目,即被包含在 #[...]#![...]属性内的东西。
  • tt: 标记树

举例来说,下列宏将其输入捕获为一个表达式:

  1. macro_rules! one_expression {
  2. ($e:expr) => {...};
  3. }

Rust编译器的语法转义器将保证捕获的“准确性”。一个expr捕获总是会捕获到一个对当前Rust版本来说完整、有效的表达式。

你可以将字面标记树与捕获混合使用,但有些限制(接下来将阐明它们)。

在扩展过程中,对于某捕获$name:kind,我们可以通过在expansion中写下$name来使用它。比如:

  1. macro_rules! times_five {
  2. ($e:expr) => {5 * $e};
  3. }

如同宏扩展本身一样,每一处捕获也都将被替换为一个完整的AST节点。也就是说,在上例中无论$e所捕获的是怎样的标记序列,它总会被解读成一个完整的表达式。

在一条模式中也可以出现多次捕获:

  1. macro_rules! multiply_add {
  2. ($a:expr, $b:expr, $c:expr) => {$a * ($b + $c)};
  3. }

重复

模式中可以包含重复。这使得匹配标记序列成为可能。重复的一般形式为$ ( ... ) sep rep.

  • $ 是字面标记。
  • ( ... ) 代表了将要被重复匹配的模式,由小括号包围。
  • sep是一个可选的分隔标记。常用例子包括,;
  • rep是重复控制标记。当前有两种选择,分别是* (代表接受0或多次重复)以及+ (代表1或多次重复)。目前没有办法指定“0或1”或者任何其它更加具体的重复计数或区间。

重复中可以包含任意有效模式,包括字面标记树,捕获,以及其它的重复。

在扩展部分,重复也采用相同的语法。

举例来说,下述宏将每一个element都通过format!转换成字符串。它将匹配0或多个由逗号分隔的表达式,并分别将它们展开成一个Vecpush语句。

  1. macro_rules! vec_strs {
  2. (
  3. // 重复开始:
  4. $(
  5. // 每次重复必须有一个表达式...
  6. $element:expr
  7. )
  8. // ...重复之间由“,”分隔...
  9. ,
  10. // ...总共重复0或多次.
  11. *
  12. ) => {
  13. // 为了能包含多条语句,
  14. // 我们将扩展部分包裹在花括号中...
  15. {
  16. let mut v = Vec::new();
  17. // 重复开始:
  18. $(
  19. // 每次重复将包含如下元素,其中
  20. // “$element”将被替换成其相应的展开...
  21. v.push(format!("{}", $element));
  22. )*
  23. v
  24. }
  25. };
  26. }
  27. #
  28. # fn main() {
  29. # let s = vec_strs![1, "a", true, 3.14159f32];
  30. # assert_eq!(&*s, &["1", "a", "true", "3.14159"]);
  31. # }