宏
ch19-06-macros.md
commit 95e931170404cb98d476b19017cbbdbc00d0834d
我们已经在本书中使用过像 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:
#![allow(unused)] fn main() { 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
}
};
}
注意:标准库中实际定义的
vec!
包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。
#[macro_export]
注解表明只要导入了定义这个宏的 crate,该宏就应该是可用的。如果没有该注解,这个宏不能被引入作用域。
接着使用 macro_rules!
和宏名称开始宏定义,且所定义的宏并 不带 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 vec
。
vec!
宏的结构和 match
表达式的结构类似。此处有一个分支模式 ( $( $x:expr ),* )
,后跟 =>
以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。这里这个宏只有一个模式,那就只有一个有效匹配方向,其他任何模式方向(译者注:不匹配这个模式)都会导致错误。更复杂的宏会有多个分支模式。
宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅 Rust 参考。
首先,一对括号包含了整个模式。我们使用美元符号($
)在宏系统中声明一个变量来包含匹配该模式的 Rust 代码。美元符号明确表明这是一个宏变量而不是普通 Rust 变量。之后是一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。$()
内则是 $x:expr
,其匹配 Rust 的任意表达式,并将该表达式命名为 $x
。
$()
之后的逗号说明一个可有可无的逗号分隔符可以出现在 $()
所匹配的代码之后。紧随逗号之后的 *
说明该模式匹配零个或更多个 *
之前的任何模式。
当以 vec![1, 2