我们已经在本书中使用过像 println! 这样的宏了,不过还没完全探索什么是宏以及它是如何工作的。
宏(Macro)指的是 Rust 中一系列的功能:使用 macro_rules! 的 声明(Declarative)宏,和三种 过程
(Procedural)宏:
• 自定义 #[derive] 宏在结构体和枚举上指定通过 derive 属性添加的代码
• 类属性(Attribute-like)宏定义可用于任意项的自定义属性
• 类函数宏看起来像函数不过作用于作为参数传递的 token
我们会依次讨论每一种宏,不过首要的是,为什么已经有了函数还需要宏呢?
宏和函数的区别
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming)。在附
录 C 中会探讨 derive 属性,其生成各种 trait 的实现。我们也在本书中使用过 println! 宏和 vec! 宏。所
有的这些宏以 展开的方式来生成比你所手写出的更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所
没有的附加能力。
一个函数标签必须声明函数参数个数和类型。相比之下,宏能够接受不同数量的参数:用一个参数调用
println !(” hello ”) 或用两个参数调用 println !(” hello {}”, name) 。而且,宏可以在编译器翻译代码前
展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时
trait 需要在编译时实现。
实现一个宏而不是一个函数的缺点是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的
Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前必须定义它,或将其引入作用域,而函数
则可以在任何地方定义和调用。
使用 macro_rules! 的声明宏用于通用元编程
Rust 最常用的宏形式是 声明宏(declarative macros)。它们有时也被称为 ”macros by example”、
”macro_rules! 宏” 或者就是 ”macros”。其核心概念是,声明宏允许我们编写一些类似 Rust match 表达
式的代码。正如在第六章讨论的那样,match 表达式是控制结构,其接收一个表达式,与表达式的结果
进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种
情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和传递给宏的源代码进行比较,同时每个模式
的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。
可以使用 macro_rules! 来定义宏。让我们通过查看 vec! 宏定义来探索如何使用 macro_rules! 结构。第
八章讲述了如何使用 vec! 宏来生成一个给定值的 vector。例如,下面的宏用三个整数创建一个 vector:
let v: Vec<u32> = vec![1, 2, 3];
也可以使用 vec! 宏来构造两个整数的 vector 或五个字符串 slice 的 vector 。但却无法使用函数做相同
的事情,因为我们无法预先知道参数值的数量和类型。
在示例 19-28 中展示了一个 vec! 稍微简化的定义。
文件名: src∕lib.rs
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
示例 19-28: 一个 vec! 宏定义的简化版本
注意:标准库中实际定义的 vec! 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例
简化,此处并没有包含在内。
无论何时导入定义了宏的包,#[macro_export] 注解说明宏应该是可用的。如果没有该注解,这个宏不
能被引入作用域。
接着使用 macro_rules! 和宏名称开始宏定义,且所定义的宏并 不带感叹号。名字后跟大括号表示宏定
义体,在该例中宏名称是 vec 。
vec! 宏的结构和 match 表达式的结构类似。此处有一个单边模式 ( $( $x:expr ),* ) ,后跟 => 以及和
模式相关的代码块。如果模式匹配,该相关代码块将被执行。假设这是这个宏中唯一的模式,则只有这
一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构
而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅参考。
首先,一对括号包含了整个模式。接下来是美元符号($ ),后跟一对括号,捕获了符合括号内模式的值
以用于替换后的代码。$ () 内则是 $x:expr ,其匹配 Rust 的任意表达式,并将该表达式记作 $x。
$ () 之后的逗号说明一个可有可无的逗号分隔符可以出现在 $ () 所匹配的代码之后。紧随逗号之后的 * 说
明该模式匹配零个或更多个 * 之前的任何模式。
当以 vec![1, 2, 3]; 调用宏时,$x 模式与三个表达式 1、2 和 3 进行了三次匹配。
现在让我们来看看与此单边模式相关联的代码块中的模式:对于每个(在 => 前面)匹配模式中的 $ () 的
部分,生成零个或更多个(在 => 后面)位于 $()* 内的 temp_vec.push() ,生成的个数取决于该模式被
匹配的次数。$x 由每个与之相匹配的表达式所替换。当以 vec![1, 2, 3]; 调用该宏时,替换该宏调用所
生成的代码会是下面这样:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的
vector 的代码。
macro_rules! 中有一些奇怪的地方。在将来,会有第二种采用 macro 关键字的声明宏,其工作方式类
似但修复了这些极端情况。在此之后,macro_rules! 实际上就过时(deprecated)了。在此基础之上,
同时鉴于大多数 Rust 程序员 使用宏而非 编写宏的事实,此处不再深入探讨 macro_rules!。请查阅在线
文档或其他资源,如 ”The Little Book of Rust Macros” 来更多地了解如何写宏。