宏是灵活扩展语言的一种方式,与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程序设计》的示例片段。