声明式宏
上一个小节,我们介绍了过程宏,可以看到,过程宏的定义都是以一个 fn macro_name(TokenStrem) -> TokenStrem 函数的形式存在
过程宏的实现,需要了解 TokenStrem的相关方法的使用;同时过程宏 是非卫生的,会受外部代码影响,也可能影响外部代码
本节内容将介绍 声明式宏,相信在之前的基础上 这一章内容不会很难
macro_rules!
我们先介绍 声明式宏的基本定义形式:
macro_rules! IDENTIFIER MacroRulesDef
macro_rules!开头,实际上它也是一个宏,该宏的定义内嵌在编译器中IDENTIFIER声明宏的名称,每个声明宏都一个名称MacroRulesDef宏规则定义 每个宏可以有一条或多条规则
我们从外往里解析 MacroRulesDef , MacroRulesDef可以有三种形式
macro_rules! double {MacroRules}
macro_rules! double [MacroRules];
macro_rules! double (MacroRules);
上面三种形式功能是一样的,习惯于使用第一种形式 在继续分解 MacroRules
macro_rules! double {
MacroRule;
MacroRule;
}
这里可以看到有多条宏的展开,这是因为声明式宏可以根据不同情况有多种展开形式
下面例子说明了这一点
#[derive(Debug)]
struct Apple(i32);
impl Drop for Apple {
fn drop(&mut self) {
println!("drop apple {:?}",self);
}
}
macro_rules! double [
($x: ident) => {let app = Apple(100); $x *= 2}; //说明卫生
($x: expr, $y: expr) => {$x*=2; $y*=2};
($x: literal) => {panic!("aaa")};
];
fn main() {
let mut a = 10;
let app = Apple(10);
double!(a);
let mut b = 10;
double!(a,b);
double!(10);
println!("still use app {:?}",app);//说明卫生
println!("a {} b {}",a,b);//说明卫生
}
让我们继续对MacroRule 进行分解 可以看到由一个 MacroMatcher + => + MacroTranscriber 组成
宏在展开时,会先根据token 尝试匹配 MacroMatcher,如果匹配到预期的 MacroMatcher 则进行对应的展开
macro_rules! double {
MacroMatcher => MacroTranscriber;
MacroMatcher => MacroTranscriber;
}
MacroMatcher
由于匹配器的内容较多 我们单独增加一个小节 尝试说明
当一个宏被调用时,macro_rules! 解释器将按照声明顺序一一检查规则。
对每条规则,它都将尝试将输入标记树的内容与该规则的matcher进行匹配。某个matcher必须与输入完全匹配才被认为是一次匹配。
如果所有规则均匹配失败,则宏展开失败并报错。
最简单的例子是空 matcher:
macro_rules! four {
() => { 1 + 3 };
}
//当且仅当匹配到空的输入时,匹配成功,即 four!()、four![] 或 four!{} 三种方式调用是匹配成功的 。
下面是一个稍微复杂的匹配
macro_rules! gibberish {
(4 fn ['spang "whammo"] @_@) => {...};
}
//使用下述匹配可以匹配成功
gibberish!(4 fn ['spang "whammo"] @_@])
从上述两个例子,可以看到,macro_rules 的匹配 本质上在匹配 宏调用时传入的 tokenStream
元变量
有时,我们除了匹配,还想要复用 输入流 中的元素(大部分情况下是这样的),这样的变量 我们也叫元变量,匹配器可以在匹配的同时 绑定匹配内容到一个变量
捕获的书写方式是:先写美元符号$,然后跟一个标识符,然后是冒号 :,最后是捕获方式,比如 $e:expr。
一旦想要捕获变量,需要描述捕获方式,捕获方式有以下类型
- block:一个块(比如一块语句或者由大括号包围的一个表达式)
- expr:一个表达式 (expression)
- ident:一个标识符 (identifier),包括关键字 (keywords)
- item:一个条目(比如函数、结构体、模块、impl 块)
- lifetime:一个生命周期注解(比如 'foo、'static)
- literal:一个字面值(比如 "Hello World!"、3.14、'🦀')
- meta:一个元信息(比如 #[...] 和 #![...] 属性内部的东西)
- pat:一个模式 (pattern)
- path:一条路径(比如 foo、::std::mem::replace、transmute::<_, int>)
- stmt:一条语句 (statement)
- tt:单棵标记树
- ty:一个类型
- vis:一个可能为空的可视标识符(比如 pub、pub(in crate))
macro_rules! times_five {
($e:expr) => { 5 * $e }; //通过捕获表达式的方式,定义元变量e 然后再 宏展开中 可以通过 $e 使用元变量
}
fn main() {
let a = 10;
let b = times_five!(a);
println!("{}",b);
}
更多关于各个捕获方式的定义,比如什么是表达式 什么是标识符 参考文档
元变量支持捕获多个,并且元变量支持重复使用
macro_rules! discard {
($e:expr) => {};
}
macro_rules! repeat {
($e:expr) => { $e; $e; $e; };
}
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => { $a * ($b + $c) };
}
反复捕获
Ok,如果你学习过正则表达式,对于贪婪匹配一定不陌生,宏的匹配器也支持贪婪匹配 基本语法为
$ ( ... ) sep rep
$是字面上的美元符号标记( ... )是被反复匹配的模式,由小括号包围sep是可选的分隔标记。它不能是括号或者反复操作符 rep。常用例子有,和;-
rep是必须的重复操作符。当前可以是?表示最多一次重复,所以此时不能前跟分隔标记*表示零次或多次重复+表示一次或多次重复
macro_rules! vec_strs {
(
// 开始反复捕获
$(
// 每个反复必须包含一个表达式
$element:expr
)
// 由逗号分隔
,
// 0 或多次
*
) => {
// 在这个块内用大括号括起来,然后在里面写多条语句
{
let mut v = Vec::new();
// 开始反复捕获
$(
// 每个反复会展开成下面表达式,其中 $element 被换成相应被捕获的表达式
v.push(format!("{}", $element));
)*
v
}
};
}
fn main() {
let s = vec_strs![1, "a", true, 3.14159f32];
assert_eq!(s, &["1", "a", "true", "3.14159"]);
}
你可以在一个反复语句里面使用多次和多个元变量,只要这些元变量以相同的次数重复。所以下面的宏代码正常运行:
macro_rules! repeat_two {
(
// 开始反复捕获
$(
// 每个反复必须包含一个表达式
$i:ident
)
//没有分隔符 直接匹配0次或多次 标识符 ident
*
//裸字符 匹配 `,`
,
// 开始反复捕获
$($i2:ident)*)
=> {
// 在这个块内用大括号括起来,然后在里面写多条语句
$( let $i: (); let $i2: (); )*
}
}
fn main () {
repeat_two!( a b c d e f, u v w x y z );
}
尝试修复下面代码
macro_rules! repeat_two {
($($i:ident)*, $($i2:ident)*) => {
$( let $i: (); let $i2: (); )*
}
}
fn main() {
repeat_two!( a b c d e f, x y z );
}
元变量表达式
元变量表达式为 代码翻译器 提供了关于元变量的信息 —— 这些信息是不容易获得的。
目前除了 $$ 表达式外,它们的一般形式都是 $ { op(...) }, 即除了 $$ 以外的所有元变量表达式都涉及反复。
可以使用以下表达式(其中 ident 是所绑定的元变量的名称,而 depth 是整型字面值):
- ${count(ident)}:最里层反复 $ident 的总次数,相当于 ${count(ident, 0)}
- ${count(ident,depth)}:第 depth 层反复 $ident 的次数
- ${index()}:最里层反复的当前反复的索引,相当于 ${index(0)}
- ${index(depth)}:在第 depth 层处当前反复的索引,向外计数
- ${length()}:最里层反复的重复次数,相当于 ${length(0)}
- ${length(depth)}:在第 depth 层反复的次数,向外计数
- ${ignore(ident)}:绑定 $ident 进行重复,并展开成空
- $$:展开为单个 $,这会有效地转义 $ 标记,因此它不会被展开(转写)
再谈分类符
block 分类符只匹配 block 表达式。
块 (block) 由 {开始,接着是一些语句,最后是可选的表达式,然后以 }结束。
块的类型要么是最后的值表达式类型,要么是 () 类型。
macro_rules! blocks {
($($block:block)*) => ( $(println!("{}", stringify!($block));)* );
}
fn main() {
blocks! {
{}
{
let zig;
}
{ 2 }
}
}
expr分类符用于匹配任何形式的表达式 expression
macro_rules! expressions {
($($expr:expr)*) => ( $(println!("{}", stringify!($expr));)* );
}
fn main() {
expressions! {
"literal"
funcall()
future.await
break 'foo bar
}
}
ident 分类符用于匹配任何形式的标识符 (identifier) 或者关键字
macro_rules! idents {
($($ident:ident)*) => ( $(println!("{}", stringify!($ident));)* );
}
fn main() {
idents! {
// _ <- `_` 不是标识符,而是一种模式
foo
async
O_________O
_____O_____
}
}
item分类符只匹配 Rust 的 item 的 定义 ,
不会匹配指向 item 的标识符 (identifiers)。例子:
macro_rules! items {
($($item:item)*) => ($(println!("{}", stringify!($item));)* );
}
fn main() {
items! {
struct Foo;
enum Bar {
Baz
}
impl Foo {}
/*...*/
}
}
lifetime分类符用于匹配生命周期注解或者标签。 它与 ident 很像,但是 lifetime 会匹配到前缀 ''
macro_rules! lifetime {
($($lifetime:lifetime)*) => ($(println!("{}", stringify!($lifetime));)* );
}
fn main() {
lifetime! {
'static
'shiv
'_
}
}
literal分类符用于匹配字面表达式
macro_rules! literals {
($($literal:literal)*) => ($(println!("{}", stringify!($literal));)* );
}
fn main() {
literals! {
-1
"hello world"
2.3
b'b'
true
}
}
meta分类符用于匹配属性 (attribute), 准确地说是属性里面的内容。
通常你会在 #[$meta:meta] 或 #![$meta:meta] 模式匹配中 看到这个分类符。
macro_rules! metas {
($(#[$meta:meta])*) => ($(println!("{}", stringify!($meta));)* );
}
fn main() {
metas! {
#[cfg]
#[dbg]
}
}
针对文档注释简单说一句: 文档注释其实是具有 #[doc="…"] 形式的属性,
...实际上就是注释字符串, 这意味着你可以在在宏里面操作文档注释!
pat分类符用于匹配任何形式的模式 pattern,
包括 2021 edition 开始的 or-patterns
macro_rules! patterns {
($($pat:pat)*) => ($(println!("{}", stringify!($pat));)* );
}
fn main() {
patterns! {
"literal"
_
0..5
ref mut PatternsAreNice
0 | 1 | 2 | 3
}
}
从 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 )|+ )*) =>
($( $( println!("{}", stringify!($pat)); )+)* );
}
fn main() {
patterns! {
"literal"
_
0..5
ref mut PatternsAreNice
0 | 1 | 2 | 3
}
}
更多细节
更多细节参考 宏小册