宏
在本书中,我们使用了像 println!
这样的宏,但我们还没有完全探讨什么是宏以及它是如何工作的。术语 macro 指的是 Rust 中的一系列功能:带有 macro_rules!
的声明式宏和三种过程宏:
自定义#[derive]
宏,用于指定使用用于结构和枚举的derive
属性添加的代码
定义可用于任何项目的自定义属性的类似属性的宏
类似函数的宏,看起来像函数调用,但对指定为其参数的标记进行作
我们将依次讨论这些,但首先,让我们看看为什么我们已经有函数了还需要宏。
宏和函数之间的区别
从根本上说,宏是一种编写代码的方式,这种方式可以编写其他代码,这称为元编程。在附录 C 中,我们讨论了 derive
属性,该属性会为您生成各种特征的实施。我们已经
还在整本书中使用了 println!
和 vec!
宏。所有这些宏都会扩展以生成比您手动编写的代码更多的代码。
元编程有助于减少您必须编写和维护的代码量,这也是函数的作用之一。但是,宏具有一些函数所没有的额外功能。
函数签名必须声明函数具有的参数的数量和类型。另一方面,宏可以采用可变数量的参数:我们可以调用 println!(”hello“)
或
println!(“hello {}”, 姓名)
带有两个参数。此外,宏在编译器解释代码的含义之前被扩展,因此宏可以在给定类型上实现 trait。函数不能,因为它在运行时被调用,并且需要在编译时实现 trait。
实现宏而不是函数的缺点是宏定义比函数定义更复杂,因为你编写的 Rust 代码会编写 Rust 代码。由于这种间接性,宏定义通常比函数定义更难阅读、理解和维护。
宏和函数之间的另一个重要区别是,在文件中调用宏之前,必须先定义宏或将其引入范围,这与可以在任何位置定义和调用的函数不同。
使用 macro_rules!
的声明式宏,用于通用元编程
Rust 中使用最广泛的宏形式是声明式宏。这些有时也称为“宏示例”、“macro_rules!
宏“或只是普通的 ”macros”。从本质上讲,声明性宏允许你编写类似于 Rust match
表达式的内容。如第 6 章所述,
match
表达式是采用表达式的控制结构,将表达式的结果值与模式进行比较,然后运行与匹配模式关联的代码。宏还将值与与特定代码关联的模式进行比较:在这种情况下,值是传递给宏的 Literal Rust 源代码;将模式与该源代码的结构进行比较;与每个模式关联的代码(如果匹配)将替换传递给宏的代码。这一切都发生在编译期间。
要定义宏,请使用 macro_rules!
构造。让我们通过查看 VEC 的
定义来探索如何使用 macro_rules!
宏。第 8 章介绍了如何使用 vec!
宏创建具有特定值的新 vector。例如,下面的宏创建一个包含三个整数的新向量:
#![allow(unused)] fn main() { let v: Vec<u32> = vec![1, 2, 3]; }
我们还可以使用 vec!
宏来生成两个整数的向量或五个字符串切片的向量。我们不能使用函数来做同样的事情,因为我们事先不知道值的数量或类型。
示例 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]
注解表示每当定义宏的 crate 进入作用域时,这个宏都应该可用。如果没有此注释,则无法将宏纳入范围。
然后,我们以 macro_rules!
和我们定义的宏的名称(不带感叹号)开始宏定义。在本例中为 name
vec
的 vec 后跟大括号,表示宏定义的主体。
vec!
body 中的结构类似于 match
的结构
表达。这里我们有一个模式为 ( $( $x:expr ),* )
的分支,后跟 =>
和与此模式关联的代码块。如果模式匹配,将发出关联的代码块。鉴于这是此宏中的唯一模式,因此只有一种有效的匹配方式;任何其他模式都会导致错误。更复杂的宏将具有多个分支。
宏定义中的有效模式语法与第 18 章中介绍的模式语法不同,因为宏模式与 Rust 代码结构而不是值匹配。让我们来看看示例 19-28 中的 pattern pieces 是什么意思;有关完整的宏模式语法,请参阅 Rust
参考资料。
首先,我们使用一组括号来包含整个模式。我们使用美元符号 ($
) 在宏系统中声明一个变量,该变量将包含与模式匹配的 Rust 代码。美元符号清楚地表明这是一个宏变量,而不是一个常规的 Rust 变量。接下来是一组括号,用于捕获与括号内的模式匹配的值,以便在替换代码中使用。$()
中是 $x:expr
,它匹配任何 Rust 表达式,并为表达式命名 $x
。
$()
后面的逗号表示文本逗号分隔符可以选择出现在与 $()
中的代码匹配的代码之后。的 *
指定模式匹配 *
前面的零个或多个内容。
当我们用 vec![1, 2, 3];
,则 $x
模式与三个表达式 1
、2
和 3
匹配三次。
现在让我们看看与此 Arm 关联的代码正文中的模式:
$()*
中的 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
}
我们定义了一个宏,它可以接受任意数量的任何类型的参数,并且可以生成代码来创建包含指定元素的向量。
要了解有关如何编写宏的更多信息,请查阅在线文档或其他资源,例如由 Daniel Keep 发起并由 Lukas Wirth 续著的“The Little Book of Rust Macros”。
用于从属性生成代码的过程宏
宏的第二种形式是过程宏,它的作用更像一个函数(并且是一种过程)。过程宏接受一些代码作为输入,对该代码进行作,并生成一些代码作为输出,而不是像声明性宏那样与模式匹配并用其他代码替换代码。这三种过程宏是自定义派生、类似属性和类似函数的宏,它们都以类似的方式工作。
在创建过程宏时,定义必须位于其自己的 crate 中,并具有特殊的 crate 类型。这是出于复杂的技术原因,我们希望将来能消除。在示例 19-29 中,我们展示了如何定义过程宏,其中 some_attribute
是使用特定宏变体的占位符。
文件名: src/lib.rs
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
示例 19-29:定义过程宏的示例
定义过程宏的函数将 TokenStream
作为输入,并生成 TokenStream
作为输出。TokenStream
类型由 Rust 附带的 proc_macro
crate 定义,表示一系列令牌。这是宏的核心:宏所作的源代码构成了输入 TokenStream
,宏生成的代码是输出 TokenStream
。该函数还附加了一个属性,用于指定我们正在创建的程序宏类型。我们可以在同一个 crate 中拥有多种过程宏。
让我们看看不同类型的过程宏。我们将从自定义 derive 宏开始,然后解释使其他形式不同的细微差异。
如何编写自定义 derive
宏
让我们创建一个名为 hello_macro
的 crate,它定义了一个名为
HelloMacro
替换为一个名为 hello_macro
的关联函数。与其让我们的用户为他们的每种类型实现 HelloMacro
trait, 不如让我们的用户为
我们将提供一个过程宏,以便用户可以使用
#[derive(HelloMacro)]
来获取 hello_macro
的默认实现
功能。默认实现将打印 Hello, Macro! My name is TypeName!
其中 TypeName
是定义此 trait 的类型的名称。换句话说,我们将编写一个 crate,使另一个程序员能够使用我们的 crate 编写示例 19-30 这样的代码。
文件名: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
示例 19-30: 我们的 crate 用户在使用我们的过程宏时能够编写的代码
完成后,将打印 Hello, Macro! My name is Pancakes!
此代码。第一步是创建一个新的 library crate,像这样:
$ cargo new hello_macro --lib
接下来,我们将定义 HelloMacro
trait 及其关联的函数:
文件名: src/lib.rs
pub trait HelloMacro {
fn hello_macro();
}
我们有一个 trait 及其功能。此时,我们的 crate 用户可以实现 trait 来实现所需的功能,如下所示:
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
但是,他们需要为他们想要与 hello_macro
一起使用的每种类型编写 implementation 块;我们希望他们不必做这项工作。
此外,我们还不能为 hello_macro
函数提供默认实现,该实现将打印实现 trait 的类型的名称:Rust 没有反射功能,因此它无法在运行时查找类型的名称。我们需要一个宏来在编译时生成代码。
下一步是定义过程宏。在撰写本文时,过程宏需要位于其自己的 crate 中。最终,此限制可能会被取消。构建 crate 和宏 crate 的约定如下:对于名为 foo
的 crate,自定义的 derive 过程宏 crate 称为 foo_derive
。让我们在 hello_macro
项目中启动一个名为 hello_macro_derive
的新 crate:
$ cargo new hello_macro_derive --lib
我们的两个 crate 密切相关,因此我们在 hello_macro
crate 的目录中创建了 proc 宏 crate。如果我们更改了 hello_macro
中的特征定义,我们也必须更改 hello_macro_derive
中过程宏的实现。这两个板条箱需要
单独发布,使用这些 crate 的程序员需要添加
两者都作为依赖项,并将它们都引入范围。我们可以改用
hello_macro
crate 使用 hello_macro_derive
作为依赖项并重新导出过程宏代码。但是,我们构建项目的方式使程序员可以使用 hello_macro
即使他们不希望
派生
功能。
我们需要将 hello_macro_derive
crate 声明为过程宏 crate。正如您将看到的,我们还需要 syn
和 quote
crate 中的功能
稍后,我们需要将它们添加为 dependencies。将以下内容添加到
Cargo.toml 文件hello_macro_derive
:
文件名: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
要开始定义过程宏,请将示例 19-31 中的代码放入 hello_macro_derive
crate 的 src/lib.rs 文件中。请注意,在我们为 impl_hello_macro
函数添加定义之前,此代码不会编译。
文件名: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
示例 19-31:大多数过程宏 crate 处理 Rust 代码所需的代码
请注意,我们已将代码拆分为 hello_macro_derive
函数(负责解析 TokenStream
)和 impl_hello_macro
函数,该函数负责转换语法树:这使得
编写过程宏更方便。外部函数中的代码
(在本例中为 hello_macro_derive
)对于您看到或创建的几乎所有程序化宏 crate 都是相同的。您在内部函数的主体(在本例中为 Body)中指定的代码(在本例中为 impl_hello_macro
)将根据过程宏的用途而有所不同。
我们引入了三个新的 crate:proc_macro
、syn
和 quote
。这
proc_macro
crate 都带有 Rust,因此我们不需要将其添加到 Cargo.toml 中的依赖项中。proc_macro
crate 是编译器的 API,它允许我们从代码中读取和作 Rust 代码。
syn
crate 将 Rust 代码从字符串解析为我们可以对其执行作的数据结构。quote
crate 将 syn
数据结构转换回 Rust 代码。这些 crate 使解析我们可能想要处理的任何类型的 Rust 代码变得更加简单:为 Rust 代码编写完整的解析器并非易事。
当我们库的用户在类型上指定 #[derive(HelloMacro)]
时,将调用 hello_macro_derive
函数。这是可能的,因为我们在这里用 proc_macro_derive
注释了 hello_macro_derive
函数,并指定了与我们的特征名称匹配的名称 HelloMacro
;这是大多数过程宏遵循的约定。
hello_macro_derive
函数首先将
TokenStream
转换为一个数据结构,然后我们可以解释并对其执行作。这就是 syn
发挥作用的地方。中的 parse
函数
syn
接受一个 TokenStream
并返回一个 DeriveInput
结构体,该结构体表示解析后的 Rust 代码。示例 19-32 显示了 DeriveInput
的相关部分
结构中,我们从解析结构 Pancakes;
字符串中得到:
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
示例 19-32:解析示例 19-30 中具有宏属性的代码时得到的 DeriveInput
实例
这个结构体的字段显示我们解析的 Rust 代码是一个单元结构体,其 ident
(标识符,意思是名称) 为 Pancakes
。这个结构体上有更多字段用于描述各种 Rust 代码;检查 SYN
DeriveInput
的文档了解更多信息。
很快,我们将定义 impl_hello_macro
函数,我们将在这里构建我们想要包含的新 Rust 代码。但在此之前,请注意 derive 宏的输出也是 TokenStream
。返回的 TokenStream
为
添加到我们的 crate 用户编写的代码中,因此当他们编译 crate 时,
他们将获得我们在修改后的
TokenStream 的 TokenStream
中。
您可能已经注意到,我们调用 unwrap
会导致
hello_macro_derive
函数在调用 syn::p arse
函数时出现 panic
这里失败了。我们的过程宏有必要在错误时 panic ,因为
proc_macro_derive
函数必须返回 TokenStream
而不是 Result
到
符合过程宏 API。我们使用
解包
;在生产代码中,您应该使用 panic!
或 expect
提供更具体的错误消息,说明出了什么问题。
现在我们有了从 TokenStream
转换带注释的 Rust 代码的代码
添加到 DeriveInput
实例中,让我们生成实现
HelloMacro trait 的 Alpha Macro
属性,如示例 19-33 所示。
文件名: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
示例 19-33:使用解析的 Rust 代码实现 HelloMacro
trait
我们使用 ast.ident
获得一个 Ident
结构实例,其中包含带注释的类型的名称(标识符)。示例 19-32 中的结构体显示,当我们在示例 19-30 中的代码上运行 impl_hello_macro
函数时,
我们得到的 ident
将具有值为 “Pancakes”
的 ident
字段。因此,示例 19-33 中的 name
变量将包含一个 Ident
结构体实例,当打印时,它将是字符串 “Pancakes”
,即示例 19-30 中结构体的名称。
quote!
宏让我们定义要返回的 Rust 代码。编译器期望与 quote 的直接结果不同!
宏的执行,所以我们需要将其转换为 TokenStream
。我们通过调用 into
方法来做到这一点,该方法使用此中间表示形式并返回所需 TokenStream
类型的值。
quote!
宏还提供了一些非常酷的模板机制:我们可以输入 #name,quote!
会将其替换为变量
name
的您甚至可以执行一些重复作,类似于常规宏的工作方式。查看 quote
crate 的文档以获取详尽的介绍。
我们希望我们的过程宏生成 HelloMacro
的实现
trait 来获取用户注解的类型,我们可以使用 #name
来获取。trait 实现有一个函数 hello_macro
,其 body 包含我们想要提供的功能:打印 Hello, Macro!我的名字是
,然后是带注释类型的名称。
这里使用的 stringify!
宏是 Rust 中内置的。它需要一个 Rust 表达式,比如 1 + 2
,并在编译时将表达式变成一个字符串文字,比如 “1 + 2”。
这与 format!
或
println!
,这些宏计算表达式,然后将结果转换为 String
。#name
input 可能是一个要按字面打印的表达式,因此我们使用 stringify!
。使用 stringify!
还可以通过在编译时将 #name
转换为 String 文本来节省分配。
此时,cargo build
应该在两个 hello_macro
中都成功完成
和 hello_macro_derive
。让我们将这些 crate 连接到示例 19-30 中的代码中,看看过程宏是如何工作的!使用 cargo new pancakes
在 projects 目录中创建一个新的二进制项目。我们需要添加
hello_macro
和 hello_macro_derive
作为 pancakes
中的依赖项
crate 的 Cargo.toml 中。如果您要发布 hello_macro
和
hello_macro_derive
crates.io,它们将是常规依赖项;如果没有,您可以指定它们作为路径
依赖关系,如下所示:
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
将示例 19-30 中的代码放入 src/main.rs 中,运行 cargo run
:它应该打印 Hello, Macro! My name is Pancakes!
HelloMacro
特征包含在内,但不包含
需要实现它的 pancakes
crate;#[derive(HelloMacro)]
添加了 trait 实现。
接下来,让我们探讨一下其他类型的过程宏与自定义派生宏有何不同。
类似属性的宏
类似属性的宏类似于自定义 derive 宏,但它们允许您创建新属性,而不是为 derive
属性生成代码。它们也更灵活:derive
仅适用于 structs 和 enum;属性也可以应用于其他项目,例如函数。下面是一个使用类似属性的宏的示例:假设你有一个名为 route
的属性,它在使用 Web 应用程序框架时对函数进行注释:
#[route(GET, "/")]
fn index() {
这个 #[route]
属性将由框架定义为过程宏。宏定义函数的签名如下所示:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
这里,我们有两个 TokenStream
类型的参数。第一个是属性的内容: GET, “/”
部分。第二个是属性附加到的项的主体:在本例中为 fn index() {}
和函数主体的其余部分。
除此之外,类似 attribute 的宏的工作方式与自定义 derive 宏相同:使用 proc-macro
crate 类型创建一个 crate,并实现一个生成所需代码的函数!
类似函数的宏
类似函数的宏定义看起来像函数调用的宏。与
macro_rules!
宏,它们比函数更灵活;例如,它们可以接受未知数量的参数。但是,macro_rules!
宏可以是
仅使用我们在本节中讨论的类似 match 的语法定义
“使用 macro_rules 的声明性宏!
通用
元编程”。类似函数的宏采用
TokenStream
参数及其定义作该 TokenStream
像其他两种类型的过程宏一样使用 Rust 代码。一个
类似函数的宏是一个 SQL!
宏,可以按如下方式调用:
let sql = sql!(SELECT * FROM posts WHERE id=1);
这个宏会解析其中的 SQL 语句,并检查它是否是
语法正确,这比
macro_rules!
macro 可以做到。sql!
宏的定义如下:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
此定义类似于自定义 derive 宏的签名:我们接收括号内的令牌,并返回我们想要生成的代码。
总结
呼!现在,你的工具箱中有一些 Rust 功能,你可能不会经常使用,但你会知道它们在非常特殊的情况下是可用的。我们介绍了几个复杂的主题,因此当您在错误消息建议或其他人的代码中遇到它们时,您将能够识别这些概念和语法。使用本章作为参考来指导您找到解决方案。
接下来,我们将把整本书中讨论的所有内容付诸实践,再做一个项目!