Rust 声明宏
Rust 宏编程
相对于C的宏, Rust的功能要强大的多, 但是也更加复杂.
由于Rust要求一切都是"explicit", 所以, 它没有C++的各种隐式类型转换, 也没有重载, 并且也没有变长参数.
但是, 如果没有这些, 那么又很难做到"人体工程学", 所以, Rust提供了宏来解决这些问题.
Rust中的宏无处不在, 比如println!, format!, vec!, 等等, 这些都是宏.
在Rust中, 宏主要做两件事
- 减少啰嗦的样板代码
- 增加语言特性(甚至可以实现自己定制的DSL)
比如初始化一个Vector:
fn foo() -> Vec<i32> {
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
return v;
}
如果用宏来写, 就是:
fn foo1() -> Vec<i32> {
vec![1, 2, 3];
}
这篇文章会尝试讲解一下Rust中的宏, 以及如何编写一些简单的宏.
声明宏(Declarative Macros)
Rust中, 宏可以大概分成两种, 一个是声明宏(Declarative Macros), 一个是过程宏(Procedural Macros).
声明宏相对比较简单, 差不多就是高级一点点的文本替换, 跟C的差不多. 虽然说, 官方的vec!宏会有一些特殊的处理, 但是为了展示过程宏的实现, 我们这里简单写一个低配版的, 就叫my_vec:
macro_rules! my_vec {
[$($x:expr), +] => ({
let mut temp_vec = Vec::new();
$(temp_vec.push($x);)+
v
})
}
它展开后的代码差不多是:
fn foo2() -> Vec<i32> {
let v = my_vec![1, 2, 3];
v
}
/* 展开后的代码 */
fn foo2() -> Vec<i32> {
let v = {
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
};
v
}
简单来说, 过程宏的语法格式基本是:
macro_rules! <macro_name> {
<pattern1> => {
<expansion1>
};
<pattern2> => {
<expansion2>
};
...
}
更详细的可以参考 https://doc.rust-lang.org/reference/macros-by-example.html 这里直接给出了过程宏定义的文法
每个过程宏都有一个名称, 以及若干个规则组成, 每个规则都是一个模式和一个展开体, 当宏被调用时, 会依次匹配规则, 如果匹配成功, 就会展开对应的展开体.
当调用过程宏时, Rust的宏处理器会按照名称来查找对应的定义, 然后尝试依次匹配宏中的每个规则, 并按照第一个成功的规则进行展开.
比如, 我们可以简单举个例子:
macro_rules! foo {
($e:expr) => {
println!("foo expr: {}", $e);
};
}
macro_rules! bar{
($l:literal) => {
println!("bar literal: {}", $l);
};
($e:expr) => {
println!("bar expr: {}", $e);
};
}
macro_rules! baz {
($e:expr) => {
println!("baz expr: {}", $e);
};
($l:literal) => {
println!("baz literal: {}", $l);
}
}
fn main(){
foo!(1);
foo!(1 + 2);
bar!(1);
bar!(1 + 2);
baz!(1);
baz!(1 + 2);
}
// Output:
// foo expr: 1
// foo expr: 3
// bar literal: 1
// bar expr: 3
// baz expr: 1
// baz expr: 3
在"bar"宏中, 我们定义了两个规则, 一个是匹配字面量, 一个是匹配表达式, 当我们调用bar!(1)时, 会匹配到第一个规则, 而bar!(1 + 2)会匹配到第二个规则. 而如果在foo中, 我们只定义了一个规则, 所以, 不管是foo!(1)还是foo!(1 + 2)都会匹配到这个规则. 而在baz中, 由于无论是1或是1 + 2, 都能匹配到第一个expr型, 所以第二个literal规则, 就永远不会被匹配到了.
讲到这里, 就不得不说的声明宏的两个重要概念了, 即元变量(Metavariables)以及重复(Repetitions).
元变量(Metavariables)以及重复(Repetitions)
元变量就是在模式中, 以$开头的变量, 比如上面的$e, l, 它们会匹配到对应的表达式或者字面量, 并且在展开体中, 会被替换成对应的值.
而重复, 则是指在模式中, 以+或者*结尾的元变量, 比如上面的$e+, 它会匹配到一个或者多个表达式, 并且在展开体中, 会按照每次匹配到的东西, 依次展开.
元变量有点类似于正则匹配中的"捕获组", 而重复则类似于正则匹配中的"量词".
元变量的声明语法为$<元变量名>:<片段选择器>. 这里用尖括号括起来的是需要替换掉的实际值, 元变量名是一个标识符, 片段选择器(specifiers)可以指定这个元变量会绑定一个什么东西.
多一嘴, 这里的片段选择器是可选的, 可加可不加
片段选择器可以是:
- item https://doc.rust-lang.org/reference/items.html
- block 代码块
- stmt 不带";"结尾的语句
- pat 模式
- expr 表达式
- ty 类型
- ident 标识符/关键字
- path 呃, 比如x::y::z这种
- tt TokenTree 单个或一组token, 基本可以指代一切
- meta 属性(https://doc.rust-lang.org/reference/attributes.html), 这是之后过程宏要讲的妙妙工具, 过程宏这块知道就行
- lifetime 生命周期标识
- vis 可见性(https://doc.rust-lang.org/reference/visibility-and-privacy.html)
- literal 字面量
这里tt是一个比较特殊的, 它可以匹配任何东西, 但是, 由于它是一个单独的token, 所以, 如果你想匹配一个表达式, 那么你需要用$e:expr而不是$e:tt, 因为$e:tt只能匹配一个token, 而$e:expr可以匹配一个表达式, 也就是多个token. 但如果内容被(), {}, []包裹, 那么tt就可以匹配这些全部内容了.
而重复(Repetitions), 则是Rust实现(或者叫模拟也可以)可变参数的妙妙工具了.
Repetition需要在模式和展开体中使用, 将需要重复的代码(token)放在$()内, 然后接一个分隔符(可选), 最后接*/+/?表示限制数量的符号.
这三个符号和正则中的意义差不多, 星号代表出现任意此(0-n), 加号代表出现至少一次(1-n), 问号代表出现0次或者1次(0-1).
Repetition有两个限制
- Repetition中必须有元变量(不然它也不知道该展开多少次了)
- 在展开体中, 元变量与它在Pattern(匹配器)出现的次数, 类型以及嵌套顺序必须一致.
这么说下去太干巴巴了, 我们来画张图来演示这个匹配过程:

接下来是存在Repitition的情况:

到这里, 声明宏差不多就结束了, 可能还会有一些或者高级, 或者"奇技淫巧"的方法, 但是可以说, 声明宏不是"图灵完全的", 无法做出很复杂的事情. 但是对于减少一些样板代码, 还是很有用的.
接下来, 我会继续讲解一下过程宏, 过程宏才是Rust中的"万能工具", 但是也是由于其复杂性更高, 所以过程宏的文章可能要稍晚一些, 预知后事如何, 且听下回分解~

浙公网安备 33010602011771号