宏是灵活扩展语言的一种方式,与C/C++一样,Rust也支持宏,并且还可以实现超越函数的功能。在编译期间,每个宏都会被替换成一些其他的Rust代码。
相比于C/C++,Rust的宏可以更好的融入整个语言中。宏在Rust的调用始终以一个 ! 标记,并且宏还自带模式匹配,所以在调用的时候也比较不容易出错。
宏调用
宏的调用是使用 ! 标记的,并且宏的调用可以使用圆括号,也可以使用方括号,还可以使用大括号。以下的三种调用方法都是合法的。
assert_eq!(gcd(6, 10), 2);assert_eq![gcd(6, 10), 2];assert_eq!{gcd(6, 10), 2}
只是需要注意的是,宏调用始终是使用 ! 标记,而且在使用大括号的时候,最后的分号是可选的。不过使用哪种括号,一般会按照惯例来选择。比如类似于函数调用的宏就会选择圆括号,列表创建的宏就会使用方括号,而类似于 macro_rules! 这样的大段代码就会使用大括号。
宏在使用的时候,必须要保证宏已经被定义或者被引入到当前的模块中。
宏定义
常用的宏定义是使用 macro_rules! 关键字来进行的。宏采用模式匹配来定义,其格式如下:
macro_rules! <宏名称> {(模式1) => (模版1);(模式2) => (模版2);}
使用关键字 macro_rules! 定义的宏完全基于模式匹配的规则实现其中的逻辑,而宏的主体就是这一系列的规则,一个宏最少包含一个模式和一个模版。
宏的模式是一个迷你型的语言,本质上是正则表达式,但与仅用来匹配字符的正则表达式不同的是,模式匹配的是记号,例如数字、名称、标点等Rust中的语法符号。Rust在扩展宏的调用时,过程很像是执行一个 match 表达式,通过对模式的匹配来确定替换的模版。
以 assert_eq 宏为例,它的一个匹配模式定义如下:
macro_rules! assert_eq {($left:expr, $right:expr) => ({match (&$left, &$right) {(left_val, right_val) => {if !(*left_val == *right_val) {panic!(); // 这里省略激活panic的内容。}}}});}
在这个示例中,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 的一个模式的定义就如同以下示例:
macro_rules! vec {( $( $x:expr ),* ) => {<[_]>::into_vec(Box::new([ $( $x ),* ]))};}
在这个示例里,不仅模式部分使用到了这种重复匹配的规则,在模版里也同样有应用。示例中通过模式匹配到的内容,会在模版中通过重复语法一一重放。
示例中的
<[_]>表示“具有某种类型值的切片”,但是未指定明确类型,类型由Rust自行推断获得。
导出与导入
宏是在编译早期被扩展的,所以宏不能像函数和结构体那样使用常规的方式导入和导出。宏的导入和导出在存在不同的包数量的情况下,有不同的特性。
在只有一个包的情况下:
- 一个模块中可见的宏会自动在其子模块中可见。
- 要在父模块中使用子模块定义的宏,需要使用
#[macro_use]属性标记子模块的定义语句。例如:
在这个示例中,模块#[macro_use] mod macros;mod analysis;mod statistics;
macros中定义的所有宏,在模块analysis和statistics中都是可见的。
在有多个包的情况下:
- 要从一个包导入宏,需要在
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程序设计》的示例片段。
