正如在 思路 一章看到的,截至 1.60 版本, Rust 已有
14 个片段分类符 (Fragment Specifiers,以下简称分类符)。
这一节会更深入地探讨他们之中的细节,每次都会展示几个匹配的例子。
注意:除了 ident
、lifetime
和 tt
分类符之外,其余的分类符在匹配后生成的
AST 是不清楚的 (opaque),这使得在之后的宏调用时不可能检查 (inspect) 捕获的结果。
block
分类符只匹配
block 表达式。
块 (block) 由 {
开始,接着是一些语句,最后是可选的表达式,然后以 }
结束。
块的类型要么是最后的值表达式类型,要么是 ()
类型。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
expr
分类符用于匹配任何形式的表达式
(expression)。
(如果把 Rust 视为面向表达式的语言,那么它有很多种表达式。)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ident
分类符用于匹配任何形式的标识符
(identifier) 或者关键字。
。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
item
分类符只匹配 Rust 的 item
的 定义 (definitions) ,
不会匹配指向 item 的标识符 (identifiers)。例子:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
item
是在编译时完全确定的,通常在程序执行期间保持固定,并且可以驻留在只读存储器中。具体指:
lifetime
分类符用于匹配生命周期注解或者标签
(lifetime or label)。
它与 ident
很像,但是 lifetime
会匹配到前缀 ''
。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
literal
分类符用于匹配字面表达式
(literal expression)。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
meta
分类符用于匹配属性 (attribute),
准确地说是属性里面的内容。通常你会在 #[$meta:meta]
或 #![$meta:meta]
模式匹配中
看到这个分类符。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
针对文档注释简单说一句:
文档注释其实是具有 #[doc="…"]
形式的属性,...
实际上就是注释字符串,
这意味着你可以在在宏里面操作文档注释!
pat
分类符用于匹配任何形式的模式
(pattern),包括 2021 edition
开始的 or-patterns。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
从 2021 edition 起, or-patterns 模式开始应用,这让 pat
分类符不再允许跟随 |
。
为了避免这个问题或者说恢复旧的 pat
分类符行为,你可以使用 pat_param
片段,它允许
|
跟在它后面,因为 pat_param
不允许 top level 或 or-patterns。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
path
分类符用于匹配类型中的路径
(TypePath)。
这包括函数式的 trait 形式。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
stmt
分类符只匹配的 语句 (statement)。
除非 item 语句要求结尾有分号,否则 不会 匹配语句最后的分号。
什么叫 item 语句要求结尾有分号呢?单元结构体 (Unit-Struct) 就是一个简单的例子,
因为它定义中必须带上结尾的分号。
赶紧用例子展示上面说的是啥意思吧。下面的宏只给出它所捕获的内容,因为有几行不能通过编译。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
你可以根据报错内容试着删除不能编译的代码,结合 stmt
小节开头的文字再琢磨琢磨。
你如果正浏览使用 mdbook 渲染的页面,那么可以直接运行和修改这段代码。
虽然源代码编译失败,但是我们可以展开宏,
使用 playground 的
Expand 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 { }
{ }
}
由此我们知道:
-
虽然 stmt
分类符没有捕获语句末尾的分号,但它依然在所需的时候返回了 (emit)
语句。原因很简单,分号本身就是有效的语句。所以我们实际输入 10 个语句调用了宏,而不是 8
个!这在把多个反复捕获放入一个反复展开时很重要,因为此时反复的次数必须相同。
-
在这里你应该注意到:struct Foo;
被匹配到了。否则我们会看到像其他情况一样有一个额外 ;
语句。由前所述,这能想通:item 语句需要分号,所以这个分号能被匹配到。
-
仅由块表达式或控制流表达式组成的表达式结尾没有分号,
其余的表达式捕获后产生的表达式会尾随一个分号(在这个例子中,正是这里出错)。
这里提到的细节能在 Reference 的 statement
一节中找到。但个细节通常这并不重要,除了要注意反复次数,通常没什么问题。
tt
分类符用于匹配标记树 (TokenTree)。
如果你是新手,对标记树不了解,那么需要回顾本书
标记树
一节。tt
分类符是最有作用的分类符之一,因为它能匹配几乎所有东西,
而且能够让你在使用宏之后检查 (inspect) 匹配的内容。
这让你可以编写非常强大的宏技巧,比如
tt-muncher 和
push-down-accumulator。
ty
分类符用于匹配任何形式的类型表达式 (type expression)。
类型表达式是在 Rust 中指代类型的语法。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
vis
分类符会匹配 可能为空 可见性修饰符 (Visibility qualifier)。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
vis
实际上只支持例子里的几种方式,因为这里的 visibility
指的是可见性,与私有性相对。而涉及这方面的内容只有与 pub
的关键字。所以,vis
在关心匹配输入的内容是公有还是私有时有用。
此外,如果匹配时,其后没有标记流,整个宏会匹配失败:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
重点在于“可能为空”。你可能想到这是隐藏了 ?
重复操作符的分类符,这样你就不用直接在反复匹配时使用
?
—— 其实你不能将它和 ?
一起在重复模式匹配中使用。
可以匹配 $vis:vis $ident:ident
,但不能匹配 $(pub)? $ident:ident
,因为 pub
表明一个有效的标识符,所以后者是模糊不清的。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
而且,搭配 tt
分类符和递归展开去匹配空标记也会导致有趣而奇怪的事情。
当 pub
匹配了空标记,元变量依然算一次被捕获,又因为它不是 tt
、ident
或
lifetime
,所以再次展开时是不清楚的。
这意味着如果这种捕获的结果传递给另一个将它视为 tt
的宏调用,你最终得到一棵空的标记树。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX