Skip to content

9.2 声明式宏

自定义宏有两种实现方式:

  • 声明式宏:通过标准库提供的macro_rules!宏实现。
  • 过程宏:通过提供编译器扩展来实现。

macro_rules 允许用户以声明的方式定义语法扩展。我们将此类扩展称为“示例宏(macros by example)”或简称为“宏”。

编译器扩展的 API 正在重新设计中,还没有正式定稿,这就是所谓的 macro 2.0。在后面,我们会体验 macro 1.1,它就是 macro 2.0 的缩微版。

下面我们来使用一个例子讲解如何使用macro_rules!实现自定义宏。 macro_rules!是标准库中为我们提供的一个编写简单宏的小工具,它本身也是用编译器扩展来实现的

首先,macro_rules!宏是一种基于模式匹配的宏,它可以将传入的代码块与预定义的模式进行匹配,并根据匹配结果执行相应的代码块,返回一个新的代码块。 这种宏主要用于代码块重复使用的场景,例如常见的vec!宏就是一个使用macro_rules!宏实现的例子。 该宏的语法比较简单,通常以macro_rules!组成,接着是宏名称和花括号包裹的匹配模式和相应的替换模式。

举个例子,我们考虑一下这样的需求:提供一个hashmap!宏,实现如下初始化 HashMap 的功能:

rust
let counts = hashmap!['A' => 0, 'C' => 0, 'G' => 0, 'T' => 0];

首先,定义 hashmap 这样一个宏名字:

rust
macro_rules! hashmap {
}

在大括号里面,我们定义宏的使用语法,以及它展开后的形态。 定义方式类似match语句的语法,expander => { transcriber }。左边的是宏扩展的语法定义,后面是宏扩展的转换机制。

语法定义的指示符(designators)以$开头,参数的类型支持itemblockstmtpatexprtyitentpathtt

下面是 Rust 宏系统中各种参数类型的表格:

参数类型描述
item表示一个 Rust 项 (item),比如结构体、函数等。
block表示一个代码块,即一对大括号括起来的语句序列。
stmt表示一个语句 (statement),也就是一个代码行。
pat表示一个模式 (pattern),用于匹配变量的值。
expr表示一个表达式,通常会返回某个值。
ty表示一个类型 (type),比如 i32String 等。
ident表示一个标识符 (identifier),即一个变量名或函数名等。
path表示一个 Rust 路径 (path),用于标识模块、结构体或函数等。
tt表示一个标记树 (token tree),它可以包含任意类型的标记,比如关键字、标识符、字符串等。

等等...

这里的需求是需要:一个表达式($key: expr),一个标识符(=>),再跟一个表达式($val: expr),因此,宏可以写成这样:

现在我们已经实现了一个hashmap! {'A'=>'1'};这样的语法了。 我们希望这个宏扩展开后的类型是 HashMap,而且进行了合理的初始化,那么我们可以使用“语句块”的方式来实现:

rust
macro_rules! hashmap {
    ($key: expr => $val: expr) => {
        {
          let mut map = ::std::collections::HashMap::new();
          map.insert($key, $val);
          map
        }
    }
}

这里的模式匹配器$key: expr => $val: expr 是一个模式匹配器,可以匹配类似 'A'=>'1' 这样的代码。 括号中符号$位于左侧,右侧的部分是规则,其中$key是一个标记树变量,需要在冒号(:)后面指定一个类型,即expr标记树类型。 它们的语法类似于我们在函数中指定参数的语法。当我们调用hashmap!宏时,会将任意标记序列作为输入,并在$key中捕获,然后由代码生成块中的相同变量引用。 expr标记类型意味着此宏只能接收表达式。$key$val会被替换为我们在调用时传递给宏的实际表达式。

现在我们希望在宏里面,可以支持重复多个这样的语法元素。我们可以使用+模式和*模式来完成。 类似正则表达式的概念,+代表一个或者多个重复,*代表零个或者多个重复。 因此,我们需要把需要重复的部分用括号括起来,并加上逗号分隔符:

rust
macro_rules! hashmap {
    ($( $key: expr => $val: expr ),*) => {{
        let mut map = ::std::collections::HashMap::new();
        map.insert($key, $val);
        map
    }}
}

最后,我们在语法扩展的部分也使用*符号,将输入部分扩展为多条insert语句。最终的结果如下所示:

rust
macro_rules! hashmap {
    ($( $key: expr => $val: expr ),*) => {{
        let mut map = ::std::collections::HashMap::new();
        $( map.insert($key, $val); )*
        map
    }}
}

fn main() {
    let counts = hashmap!['A' => 0, 'C' => 0, 'G' => 0, 'T' => 0];
    println!("{:?}", counts);
}

一个自定义宏就诞生了。如果我们想检查一下宏展开的情况是否正确,可以使用如下rustc的内部命令:

sh
rustc -Z unstable-options --pretty=expanded temp.rs

可以看到,hashmap!宏展开后的结果是:

rust
let counts =
        {
            let mut map = ::std::collections::HashMap::new();
            map.insert('A', 0);
            map.insert('C', 0);
            map.insert('G', 0);
            map.insert('T', 0);
            map
        };

很大一部分宏的需求我们都可以通过这种方式实现,它比较适合写那种一个模子套出来的重复代码。

9.2.1 内置宏

标准库中的内置宏 除了 println! 之外,标准库中还有很多其他非常有用的宏例如,println!vec!。它们是通过macro_rules!宏实现的。 了解它们将有助于我们以更简洁的方式提供宏应用的解决方案和了解宏的应用场景,同时不牺牲可读性。

其中一些宏如下所示:

  • dbg!:这个命令是对eprintln的封装。 通过它打印内容,输出内容会带文件名,行号等信息,可以很方便的程序调试。

  • compile_error!:此宏可用于在编译期从代码中报告错误。 当你构建自己的宏,并希望向用户报告任何语法或语义错误时,这是一个方便的选择。

  • concat!:此宏可以用来链接传递给它的任意数量的文字,并将链接的文字作为 &'static str 返回。

  • env!:此宏用于检查编译期的环境变量。在很多语言中,从环境变量访问值主要是在运行时完成的。 在 Rust 中,通过使用此宏,你可以在编译期解析环境变量。 请注意,当找不到定义的变量时,此宏会引发灾难性故障。因此它的安全版本是option_env!

  • eprint!eprintln!:此宏与 println!类似,不过会将消息输出到标准异常流。

  • include_bytes!:此宏可以作为一种将文件读取为字节数组的快捷方式,例如 &'static [u8; N]。 给定的文件路径是相对于调用此宏的当前文件解析获得的。

  • stringify!:如果希望获得类型或标记作为字符串的字面转换,那么此宏将会非常有用。 当我们编写自己的过程宏时,将会用到它。

如果想要了解标准库中可用的所有宏,可以访问官方文档

Released under the MIT License