宏是灵活扩展语言的一种方式,与C/C++一样,Rust也支持宏,并且还可以实现超越函数的功能。在编译期间,每个宏都会被替换成一些其他的Rust代码。

相比于C/C++,Rust的宏可以更好的融入整个语言中。宏在Rust的调用始终以一个 ! 标记,并且宏还自带模式匹配,所以在调用的时候也比较不容易出错。

宏调用

宏的调用是使用 ! 标记的,并且宏的调用可以使用圆括号,也可以使用方括号,还可以使用大括号。以下的三种调用方法都是合法的。

  1. assert_eq!(gcd(6, 10), 2);
  2. assert_eq![gcd(6, 10), 2];
  3. assert_eq!{gcd(6, 10), 2}

只是需要注意的是,宏调用始终是使用 ! 标记,而且在使用大括号的时候,最后的分号是可选的。不过使用哪种括号,一般会按照惯例来选择。比如类似于函数调用的宏就会选择圆括号,列表创建的宏就会使用方括号,而类似于 macro_rules! 这样的大段代码就会使用大括号。

宏在使用的时候,必须要保证宏已经被定义或者被引入到当前的模块中。

宏定义

常用的宏定义是使用 macro_rules! 关键字来进行的。宏采用模式匹配来定义,其格式如下:

  1. macro_rules! <宏名称> {
  2. (模式1) => (模版1);
  3. (模式2) => (模版2);
  4. }

使用关键字 macro_rules! 定义的宏完全基于模式匹配的规则实现其中的逻辑,而宏的主体就是这一系列的规则,一个宏最少包含一个模式和一个模版。

宏的模式是一个迷你型的语言,本质上是正则表达式,但与仅用来匹配字符的正则表达式不同的是,模式匹配的是记号,例如数字、名称、标点等Rust中的语法符号。Rust在扩展宏的调用时,过程很像是执行一个 match 表达式,通过对模式的匹配来确定替换的模版。

assert_eq 宏为例,它的一个匹配模式定义如下:

  1. macro_rules! assert_eq {
  2. ($left:expr, $right:expr) => ({
  3. match (&$left, &$right) {
  4. (left_val, right_val) => {
  5. if !(*left_val == *right_val) {
  6. panic!(); // 这里省略激活panic的内容。
  7. }
  8. }
  9. }
  10. });
  11. }

在这个示例中,expr 标记表示匹配一个表达式片段,是一个表达式片段类型,assert_eq 宏将会把匹配到的表达式替换掉相应的 $left$right 。除了 expr 标记以外,宏定义还可以使用以下片段类型标记。

片段类型 匹配内容 内容示例 允许使用的其他匹配符号
expr 表达式 表达式 => , ;
stmt 单条语句,不含分号 let a = 32 => , ;
ty 类型 i32、String => , ; ` `{ [ : > as where
path 路径 ::std::cmp::PartialOrd => , ; ` `{ [ : > as where
pat 普通模式匹配 Some(val) => , = ` `if in
item 特性项 不限
block 代码块,大括号括起来的多条语句 不限
meta 属性体 不限
ident 标识符,包括函数名、变量名等 不限
tt 记号树,语法树 不限

被匹配的片段类型仅可以在模式部分出现,在模版中是不能出现的,否则会被Rust当作一个替换结果,而不是报错。表格中允许使用的其他匹配符号也是在模式匹配的时候提供限制的,比如模式 $left:expr ~ $right:expr 就是一个不合法的模式,因为片段类型 expr 后面不能使用 ~ 符号,但是 $left:expr => $right:expr 就是一个合法的模式。

在宏的定义里面可以使用递归调用,但是需要注意的是Rust的编译器默认限制最多只能递归调用64次,如果需要提升这个限制,需要在包的顶部使用属性 #![recursion_limit = "256"] 来提升。

在进行模式匹配的时候,常常会遇到需要重复匹配的情况,就像是一个函数可以接收多个相同类型的参数。这可以使用宏模式提供的重复规则定义来实现,这个规则与正则表达式很像,但是书写的时候需要注意其中的细微区别。

  • $( ... )* ,匹配0或多次,匹配项之间没有任何分隔符。
  • $( ... ),* ,匹配0或多次,匹配项之间使用 , 分割。
  • $( ... );* ,匹配0或多次,匹配项之间使用 ; 分割。
  • $( ... )+ ,匹配1或多次,匹配项之间没有任何分割符。
  • $( ... ),+ ,匹配1或多次,匹配项之间使用 , 分割。
  • $( ... );+ ,匹配1或多次,匹配项之间使用 ; 分割。

注意 $() 之间的空格,这是必要的内容,用于将匹配模式与分隔符区分开来。

例如宏 vec 的一个模式的定义就如同以下示例:

  1. macro_rules! vec {
  2. ( $( $x:expr ),* ) => {
  3. <[_]>::into_vec(Box::new([ $( $x ),* ]))
  4. };
  5. }

在这个示例里,不仅模式部分使用到了这种重复匹配的规则,在模版里也同样有应用。示例中通过模式匹配到的内容,会在模版中通过重复语法一一重放。

示例中的 <[_]> 表示“具有某种类型值的切片”,但是未指定明确类型,类型由Rust自行推断获得。

导出与导入

宏是在编译早期被扩展的,所以宏不能像函数和结构体那样使用常规的方式导入和导出。宏的导入和导出在存在不同的包数量的情况下,有不同的特性。

在只有一个包的情况下:

  • 一个模块中可见的宏会自动在其子模块中可见。
  • 要在父模块中使用子模块定义的宏,需要使用 #[macro_use] 属性标记子模块的定义语句。例如:
    1. #[macro_use] mod macros;
    2. mod analysis;
    3. mod statistics;
    在这个示例中,模块 macros 中定义的所有宏,在模块 analysisstatistics 中都是可见的。

在有多个包的情况下:

  • 要从一个包导入宏,需要在 extern crate 引用声明上使用 #[macro_use]
  • 要从一个包中导出宏,需要在要公开的宏前面使用 #[macro_export] 进行标记。

    derive 属性

    derive 属性在结构体定义上经常可以看到,例如常常会用到的 #[derive(Debug)]derive 属性用来自动为数据结构生成一些默认的方法。derive 属性使用了Rust中的MetaListPaths语法,即类似于 #[derive()] 形式的语法,需要使用的宏要以逗号分隔之后列举在圆括号中。

能够使用在 derive 属性中的宏是一个过程宏。过程宏通常利用 proc_macro 模块来编写。标准库中提供可以直接使用的常用过程宏主要有以下这些。

  • Copy ,生成特性 Copy 所需要的代码。用于显式指定复制一个对象,一般适用于所有成员都实现了 Copy 特性。
  • Clone ,生成特性 Clone 所需要的代码。用于显式指定复制一个对象。
  • Debug ,生成特性 Debug 所需要的代码。用于支持在格式字符串中使用 {:?} 进行调试输出。
  • Default ,生成特性 Default 所需要的代码。提供使用 Default::default() 完成默认值初始化的能力。
  • Eq ,生成特性 Eq 所需要的代码。用于实现全等的判断。
  • Hash ,生成特性 Hash 所需要的代码。用于在所有成员都实现了 Hash 特性之后使数据类型利用成员的hash计算整体的hash。
  • Ord ,生成特性 Ord 所需要的代码。用于实现全部成员参加的排序。
  • PartialEq ,生成特性 PartialEq 所需要的代码。用于实现使用部分成员进行相等判断。
  • PartialOrd ,生成特性 PartialOrd 所需要的代码。用于实现使用部分成员进行排序。

    常用的宏

  • format!() ,使用一个字符串模版来构建字符串。

  • println!()print!() ,将格式化以后的文本写入标准输出流。
  • writeln!()write!() ,将格式化以后的文本写入指定流。
  • panic!() ,用一个格式化以后的文本构建一个panic。
  • vec![] ,通过给定的一系列值构建一个 Vec 实例。
  • assert!() ,断言给定的表达式的值是否为 true
  • assert_eq!() ,断言给定的两个表达式是否相等。
  • assert_ne!() ,断言给定的两个表达式是否不等。
  • concat!() ,将给定的多个表达式连接成为一个字符串。
  • file!() ,返回当前的文件名,辅助自定义宏的编写。
  • line!() ,返回当前行位置,辅助自定义宏的编写。
  • column!() ,返回当前列的位置,辅助自定义宏的编写。
  • stringify!() ,将一个语法树转换成一个字符串。
  • todo!() ,标记一个未完成的代码位置。

后记声明:文中借用了一些来自《Rust程序设计》的示例片段。