片段分类符

正如在 思路 一章看到的,截至 1.60 版本, Rust 已有 14 个片段分类符 (Fragment Specifiers,以下简称分类符)1

这一节会更深入地探讨他们之中的细节,每次都会展示几个匹配的例子。

注意:除了 identlifetimett 分类符之外,其余的分类符在匹配后生成的 AST 是不清楚的 (opaque),这使得在之后的宏调用时不可能检查 (inspect) 捕获的结果。2

1

最新内容可参考 ReferenceMetavariables 一节。

2

推荐通过 rust quiz #9 来理解这句话。

block

block 分类符只匹配 block 表达式

块 (block) 由 { 开始,接着是一些语句,最后是可选的表达式,然后以 } 结束。 块的类型要么是最后的值表达式类型,要么是 () 类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

expr

expr 分类符用于匹配任何形式的表达式 (expression)。

(如果把 Rust 视为面向表达式的语言,那么它有很多种表达式。)

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

ident

ident 分类符用于匹配任何形式的标识符 (identifier) 或者关键字。 。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

item

item 分类符只匹配 Rust 的 item定义 (definitions) , 不会匹配指向 item 的标识符 (identifiers)。例子:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

item 是在编译时完全确定的,通常在程序执行期间保持固定,并且可以驻留在只读存储器中。具体指:

lifetime

lifetime 分类符用于匹配生命周期注解或者标签 (lifetime or label)。 它与 ident 很像,但是 lifetime 会匹配到前缀 ''

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

literal

literal 分类符用于匹配字面表达式 (literal expression)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

meta

meta 分类符用于匹配属性 (attribute), 准确地说是属性里面的内容。通常你会在 #[$meta:meta]#![$meta:meta] 模式匹配中 看到这个分类符。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

针对文档注释简单说一句: 文档注释其实是具有 #[doc="…"] 形式的属性,... 实际上就是注释字符串, 这意味着你可以在在宏里面操作文档注释!

pat

pat 分类符用于匹配任何形式的模式 (pattern),包括 2021 edition 开始的 or-patterns

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

pat_param

从 2021 edition 起, or-patterns 模式开始应用,这让 pat 分类符不再允许跟随 |

为了避免这个问题或者说恢复旧的 pat 分类符行为,你可以使用 pat_param 片段,它允许 | 跟在它后面,因为 pat_param 不允许 top level 或 or-patterns。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

path

path 分类符用于匹配类型中的路径 (TypePath)。

这包括函数式的 trait 形式。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

stmt

stmt 分类符只匹配的 语句 (statement)。 除非 item 语句要求结尾有分号,否则 不会 匹配语句最后的分号。

什么叫 item 语句要求结尾有分号呢?单元结构体 (Unit-Struct) 就是一个简单的例子, 因为它定义中必须带上结尾的分号。

赶紧用例子展示上面说的是啥意思吧。下面的宏只给出它所捕获的内容,因为有几行不能通过编译。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你可以根据报错内容试着删除不能编译的代码,结合 stmt 小节开头的文字再琢磨琢磨。 你如果正浏览使用 mdbook 渲染的页面,那么可以直接运行和修改这段代码。

虽然源代码编译失败,但是我们可以展开宏3, 使用 playgroundExpand macros 工具 (tool);或者把代码复制到本地,在 nightly Rust 版本中使用 cargo rustc -- -Zunstable-options --pretty=expanded 命令得到宏展开结果:

warning: unnecessary trailing semicolon --> src/main.rs:10:20 | 10 | let zig = 3; | ^ help: remove this semicolon | = note: `#[warn(redundant_semicolons)]` on by default warning: unnecessary trailing semicolon --> src/main.rs:12:10 | 12 | 3; | ^ help: remove this semicolon #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2018::*; #[macro_use] extern crate std; macro_rules! statements { ($ ($ stmt : stmt) *) => ($ ($ stmt) *) ; } fn main() { struct Foo; fn foo() { } let zig = 3; let zig = 3; ; 3; 3; ; if true { } else { } { } }

由此我们知道:

  1. 虽然 stmt 分类符没有捕获语句末尾的分号,但它依然在所需的时候返回了 (emit) 语句。原因很简单,分号本身就是有效的语句。所以我们实际输入 10 个语句调用了宏,而不是 8 个!这在把多个反复捕获放入一个反复展开时很重要,因为此时反复的次数必须相同。

  2. 在这里你应该注意到:struct Foo; 被匹配到了。否则我们会看到像其他情况一样有一个额外 ; 语句。由前所述,这能想通:item 语句需要分号,所以这个分号能被匹配到。

  3. 仅由块表达式或控制流表达式组成的表达式结尾没有分号, 其余的表达式捕获后产生的表达式会尾随一个分号(在这个例子中,正是这里出错)。

这里提到的细节能在 Reference 的 statement 一节中找到。但个细节通常这并不重要,除了要注意反复次数,通常没什么问题。

3

可阅读 调试 一章

tt

tt 分类符用于匹配标记树 (TokenTree)。 如果你是新手,对标记树不了解,那么需要回顾本书 标记树 一节。tt 分类符是最有作用的分类符之一,因为它能匹配几乎所有东西, 而且能够让你在使用宏之后检查 (inspect) 匹配的内容。

这让你可以编写非常强大的宏技巧,比如 tt-muncherpush-down-accumulator

ty

ty 分类符用于匹配任何形式的类型表达式 (type expression)。

类型表达式是在 Rust 中指代类型的语法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

vis

vis 分类符会匹配 可能为空 可见性修饰符 (Visibility qualifier)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

vis 实际上只支持例子里的几种方式,因为这里的 visibility 指的是可见性,与私有性相对。而涉及这方面的内容只有与 pub 的关键字。所以,vis 在关心匹配输入的内容是公有还是私有时有用。

此外,如果匹配时,其后没有标记流,整个宏会匹配失败:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

重点在于“可能为空”。你可能想到这是隐藏了 ? 重复操作符的分类符,这样你就不用直接在反复匹配时使用 ? —— 其实你不能将它和 ? 一起在重复模式匹配中使用。

可以匹配 $vis:vis $ident:ident,但不能匹配 $(pub)? $ident:ident,因为 pub 表明一个有效的标识符,所以后者是模糊不清的。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

而且,搭配 tt 分类符和递归展开去匹配空标记也会导致有趣而奇怪的事情。

pub 匹配了空标记,元变量依然算一次被捕获,又因为它不是 ttidentlifetime,所以再次展开时是不清楚的。

这意味着如果这种捕获的结果传递给另一个将它视为 tt 的宏调用,你最终得到一棵空的标记树。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX