片段分类符

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

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

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

1

最新内容可参考 ReferenceMetavariables 一节。 2: 推荐通过 rust quiz #9 来理解这句话。

block

block 分类符只匹配 block 表达式

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

macro_rules! blocks {
    ($($block:block)*) => ();
}

blocks! {
    {}
    {
        let zig;
    }
    { 2 }
}
fn main() {}

expr

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

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

macro_rules! expressions {
    ($($expr:expr)*) => ();
}

expressions! {
    "literal"
    funcall()
    future.await
    break 'foo bar
}
fn main() {}

ident

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

macro_rules! idents {
    ($($ident:ident)*) => ();
}

idents! {
    // _ <- `_` 不是标识符,而是一种模式
    foo
    async
    O_________O
    _____O_____
}
fn main() {}

item

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

macro_rules! items {
    ($($item:item)*) => ();
}

items! {
    struct Foo;
    enum Bar {
        Baz
    }
    impl Foo {}
    /*...*/
}
fn main() {}

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

lifetime

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

macro_rules! lifetimes {
    ($($lifetime:lifetime)*) => ();
}

lifetimes! {
    'static
    'shiv
    '_
}
fn main() {}

literal

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

macro_rules! literals {
    ($($literal:literal)*) => ();
}

literals! {
    -1
    "hello world"
    2.3
    b'b'
    true
}
fn main() {}

meta

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

macro_rules! metas {
    ($($meta:meta)*) => ();
}

metas! {
    ASimplePath
    super::man
    path = "home"
    foo(bar)
}
fn main() {}

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

pat

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

macro_rules! patterns {
    ($($pat:pat)*) => ();
}

patterns! {
    "literal"
    _
    0..5
    ref mut PatternsAreNice
    0 | 1 | 2 | 3 
}
fn main() {}

pat_param

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

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

macro_rules! patterns {
    (pat: $pat:pat) => {
        println!("pat: {}", stringify!($pat));
    };
    (pat_param: $($pat:pat_param)|+) => {
        $( println!("pat_param: {}", stringify!($pat)); )+
    };
}
fn main() {
    patterns! {
       pat: 0 | 1 | 2 | 3
    }
    patterns! {
       pat_param: 0 | 1 | 2 | 3
    }
}
macro_rules! patterns {
    ($( $( $pat:pat_param )|+ )*) => ();
}

patterns! {
    "literal"
    _
    0..5
    ref mut PatternsAreNice
    0 | 1 | 2 | 3 
}
fn main() {}

path

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

这包括函数式的 trait 形式。

macro_rules! paths {
    ($($path:path)*) => ();
}

paths! {
    ASimplePath
    ::A::B::C::D
    G::<eneri>::C
    FnMut(u32) -> ()
}
fn main() {}

stmt

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

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

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

macro_rules! statements {
    ($($stmt:stmt)*) => ($($stmt)*);
}

fn main() {
    statements! {
        struct Foo;
        fn foo() {}
        let zig = 3
        let zig = 3;
        3
        3;
        if true {} else {}
        {}
    }
}

你可以根据报错内容试着删除不能编译的代码,结合 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 中指代类型的语法。

macro_rules! types {
    ($($type:ty)*) => ();
}

types! {
    foo::bar
    bool
    [u8]
    impl IntoIterator<Item = u32>
}
fn main() {}

vis

vis 分类符会匹配 可能为空 的内容。 (Visibility qualifier)。

macro_rules! visibilities {
    //         ∨~~注意这个逗号,`vis` 分类符自身不会匹配到逗号
    ($($vis:vis,)*) => ();
}

visibilities! {
    , // 没有 vis 也行,因为 $vis 隐式包含 `?` 的情况
    pub,
    pub(crate),
    pub(in super),
    pub(in some_path),
}
fn main() {}

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

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

macro_rules! non_optional_vis {
    ($vis:vis) => ();
}
non_optional_vis!();
// ^^^^^^^^^^^^^^^^ error: missing tokens in macro arguments
fn main() {}

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

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

macro_rules! vis_ident {
    ($vis:vis $ident:ident) => ();
}
vis_ident!(pub foo); // this works fine

macro_rules! pub_ident {
    ($(pub)? $ident:ident) => ();
}
pub_ident!(pub foo);
        // ^^^ error: local ambiguity when calling macro `pub_ident`: multiple parsing options: built-in NTs ident ('ident') or 1 other option.
fn main() {}

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

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

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

macro_rules! it_is_opaque {
    (()) => { "()" };
    (($tt:tt)) => { concat!("$tt is ", stringify!($tt)) };
    ($vis:vis ,) => { it_is_opaque!( ($vis) ); }
}
fn main() {
    // this prints "$tt is ", as the recursive calls hits the second branch with
    // an empty tt, opposed to matching with the first branch!
    println!("{}", it_is_opaque!(,));
}