你或许没有见过 Rust 宏的实现,但是你一定使用过它。因为我们第一次学习 Rust 编程的时候,写下的第一句“hello world”就是通过 println! 这个宏来打印的。它大概是 Rust 中一个最常用的宏,可以看到它和函数最大的区别是:它在调用时多了一个“!”,除此之外还有 vec!、assert_eq!都是相当常用的,可以说宏在 Rust 中无处不在。
如果你使用的宏够多,你肯定会发现一个问题,为什么有些宏使用 () 调用,有些使用[]调用,还有使用 {} 的。其实,这三种方法都能使用:
fn main() {
println!("hello world");
println!["hello world"];
println!{"hello world"}
}
只不过, println! 使用 () ,vec!使用 [] ,只是一个约定俗成。
声明式宏
Rust 有两类宏,声明式宏( declarative macros ) macro_rules! 和过程宏( procedural macros )。
过程宏相对比较复杂,这个我们以后再说。
声明式宏使得我们可以创建出语法上类似于内置 match 表达式的自定义代码结构。match 作为一种控制流构造,它接受一个表达式,并将其结果与多个预定义模式进行比较。当某个模式成功匹配时,相应的代码段即刻执行,这提供了一种直观而简洁的方式来处理多种可能的情况。借助声明式宏的力量,开发者不仅能够模拟 match 的行为,还能根据特定需求定制更加复杂和灵活的匹配逻辑,从而极大提升了Rust代码的表达能力和可维护性。
我们用它能干什么?
代码生成
其实大多数库作者用声明宏,基本都是用来消除一些样板代码,毕竟没有谁愿意写重复的工作内容。
通过宏可以自动生成重复性高、结构相似的代码。这有助于减少手动编写大量冗余代码的工作量,并提高代码的一致性和可维护性。
简化API
开发者可以使用宏来创建更简洁和易用的API接口。例如,通过宏可以将复杂的配置选项或初始化过程封装起来,使调用方只需提供少量参数即可完成操作。典型的如 vec!
DSL
还有一部分人使用声明宏实现小型DSL。DSL可以让特定领域的逻辑表达更加直观,提升开发效率和代码的可读性。
实现一个声明宏
声明宏不像属性宏那样需要单独在一个包中声明。我们可以像定义函数一样定义声明宏。
声明宏使用 macro_rules! 来定义,如果你还需要导出给外部使用,还要添加#[macro_export] 。看起来像这样:
#[macro_export]
macro_rules! my_macro {
( $( $x:expr ),* ) => {
{
//...
}
};
}
我们以一个实例的例子出发,看看如何使用声明宏来消除样板代码。
假设我们在业务中需要定义一个枚举,看起来像这样的:
pub enum Value {
None,
Bool(bool),
Int8(i8),
Int16(i16),
Int32(i32),
Long(i64),
Float(f32),
Double(f64),
}
一般我们需要为这个枚举实现一系列的 From
impl From<bool> for Value {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
如果我们不实现声明式宏,就得老老实实的写7次上面这个类似的代码,上面只有8个成员还好,实现的业务可能远远不止8个呢?让我们看看怎么实现一个声明宏来消除这些重复代码。
macro_rules! impl_from_for_field {
( $($t: ty, $o: ident ),+ ) => {$(
impl From<$t> for Value {
fn from(v: $t) -> Self {
Self::$o(v as _)
}
}
)*};
}
让我们解释一下这段声明宏:
macro_rules!是 Rust 中定义声明宏的关键字。impl_from_for_field是宏的名字。( $($t: ty, $o: ident ),+ )是宏的参数模式:- 最外面的圆括号
()将整个宏模式包裹 $()括号中模式相匹配的值会被捕获,然后用于代码替换。$t: ty,该模式中的ty表示会匹配任何 Rust 类型,并给予该模式一个名称$t$o: ident,该模式中的ident表示会匹配任何 Rust 表达式,并给予该模式一个名称$o
- 最外面的圆括号
上面的 ty 和ident 被称为 MacroFragSpec(宏片段说明符),MacroFragSpec 的值只可能是以下几个: item |block |stmt |pat_param |pat | expr | expr_2021 | ident | lifetime | literal | meta | path | tt | ty | vis 。
下面是一些说明:
- item:一个
Item(Function、Struct、等 ) - block:一个
BlockExpression块表达式({}) - stmt:不带尾部分号的
Statement(需要分号的itemStatement除外) - pat_param:一个
PatternNoTopAlt - pat:至少任何
PatternNoTopAlt,并且可能更多,具体取决于版本 - expr:除
UnderscoreExpression和ConstBlockExpression之外的Expression(参见 macro.decl.meta.expr-underscore) - expr_2021:与
expr相同(参见 macro.decl.meta.edition2021) - ty:类型
- ident:
IDENTIFIER_OR_KEYWORD或RAW_IDENTIFIER - path:
TypePath样式路径 - tt:一个
TokenTree(匹配分隔符()、[]或{}中的单个令牌或令牌序列) - meta:一个
Attr,即一个 attribute 的内容 - lifetime:一个
LIFETIME_TOKEN - vis:可能为空的
Visibility限定符 - literal:匹配
?LiteralExpression(文本表达式)
$($t: ty, $o: ident ),+ 中的 + 有点类似于正则表达式,就是表示1个或者多个的意思。
所以我们在调用的时候,可以一次性这样调用:
impl_from_for_field! {
bool, Bool,
i8, Int8,
i16, Int16,
i32, Int32,
i64, Long,
f32, Float,
f64, Double
}
除了+外,还有另外两种
*— 表示任意数量的重复。?— 表示出现次数为零或出现一次的可选片段。
有兴趣或者有需求的朋友可以了解一下其他宏片段说明符的用法。