宏
我们使用了println!
贯穿这本书,但我们还没有完全
探讨了什么是宏及其工作原理。术语 macro 是指一个族
of features in Rust:声明式宏macro_rules!
和三种
过程宏:
- 习惯
#[derive]
指定使用derive
属性 用于结构体和枚举 - 定义可用于任何项目的自定义属性的类似属性的宏
- 类似函数的宏,看起来像函数调用,但对标记进行作 指定为其参数
我们将依次讨论这些,但首先,让我们看看为什么我们甚至 需要宏。
宏和函数之间的区别
从根本上说,宏是一种编写代码的方式,而这种方式可以编写其他代码,而
称为元编程。在附录 C 中,我们讨论了derive
属性,该属性会为您生成各种特征的实施。我们已经
还使用了println!
和vec!
宏贯穿全书。所有这些
宏扩展以生成比您手动编写的代码更多的代码。
元编程有助于减少您必须编写的代码量,并且 maintain,这也是函数的作用之一。但是,宏具有 一些功能没有的额外能力。
函数签名必须声明参数的数量和类型
函数有。另一方面,宏可以采用可变数量的
参数:我们可以调用println!("hello")
替换为一个参数或println!("hello {}", name)
带有两个参数。此外,宏也得到了扩展
在编译器解释代码的含义之前,宏可以,对于
example,在给定类型上实现 trait。函数不能,因为它获取
在运行时调用,并且需要在编译时实现 trait。
实现宏而不是函数的缺点是宏 定义比函数定义更复杂,因为您正在编写 编写 Rust 代码的 Rust 代码。由于这种间接性,宏定义是 通常比 Function 更难阅读、理解和维护 定义。
宏和函数之间的另一个重要区别是您必须 定义宏或将它们引入范围,然后再在文件中调用它们,如 而不是你可以在任何地方定义和调用的函数。
声明式宏macro_rules!
用于通用元编程
Rust 中使用最广泛的宏形式是声明式宏。这些
有时也称为 “宏 by example”、”macro_rules!
宏,”
或者只是普通的 “宏”。从本质上讲,声明式宏允许您编写
类似于 Rust 的东西match
表达。如第 6 章所述,match
表达式是采用表达式的控制结构,则比较
结果值传递给 patterns,然后运行关联的代码
替换为匹配的模式。宏还会将值与以下
associated with particular code:在这种情况下,值是文本
传递给宏的 Rust 源代码;这些模式与
该源代码的结构;以及与每个模式关联的代码,当
matched,替换传递给宏的代码。这一切都发生在
汇编。
要定义宏,请使用macro_rules!
构建。让我们探索一下如何
用macro_rules!
通过查看vec!
macro 的定义。第八章
介绍了如何使用vec!
宏创建一个新向量,其中特定的
值。例如,下面的宏创建一个包含三个
整数:
#![allow(unused)] fn main() { let v: Vec<u32> = vec![1, 2, 3]; }
我们还可以使用vec!
宏创建两个整数的向量或向量
的 5 个字符串切片。我们不能使用函数来做同样的事情
因为我们事先不知道值的数量或类型。
示例 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]
annotation 表示应该创建此宏
每当定义宏的 crate 被带入时可用
范围。如果没有此注释,则无法将宏纳入范围。
然后,我们从macro_rules!
和
宏。在本例中为 namevec
后跟大括号,表示宏定义的正文。
结构体中的vec!
body 的结构类似于match
表达。这里我们有一个带有 pattern 的手臂( $( $x:expr ),* )
,
后跟和与此模式关联的代码块。如果
pattern 匹配时,将发出关联的代码块。鉴于这个
是此宏中的唯一模式,则只有一种有效的匹配方式;任何
其他模式将导致错误。更复杂的宏将具有超过
一只手臂。=>
宏定义中的有效模式语法与模式语法不同 在第 18 章中介绍,因为宏模式与 Rust 代码匹配 结构而不是值。让我们来看看这些 pattern 的组成部分 清单 19-28 的意思是;有关完整的宏模式语法,请参阅 Rust 参考资料。
首先,我们使用一组括号来包含整个模式。我们使用
美元符号 () 在宏系统中声明一个变量,该变量将包含
与模式匹配的 Rust 代码。美元符号清楚地表明这是一个
宏变量,而不是常规的 Rust 变量。接下来是一组
括号,用于捕获与括号内的模式匹配的值
用于替换代码。Within 是$
$()
$x:expr
,它与任何
Rust 表达式,并为表达式指定名称$x
.
逗号后面的逗号表示文本逗号分隔符
可以选择性地出现在与 中的代码匹配的代码之后。指定模式匹配零个或多个 .$()
$()
*
*
当我们用vec![1, 2, 3];
这$x
pattern 匹配三个
times 替换为三个表达式1
,2
和3
.
现在让我们看看与此 Arm 关联的代码正文中的模式:temp_vec.push()
Within 为阵列中匹配的每个部件生成零次或多次,具体取决于阵列的次数
比赛。这$()*
$()
$x
将替换为匹配的每个表达式。当我们调用此
macro 替换为vec![1, 2, 3];
,则生成的用于替换此宏调用的代码
将如下所示:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
我们定义了一个宏,它可以接受任意数量的任何类型的参数,并且可以 生成代码以创建包含指定元素的向量。
要了解有关如何编写宏的更多信息,请查阅在线文档或 其他资源,例如 “The Little Book of Rust Macros” 开始于 Daniel Keep 和 Lukas Wirth 继续。
用于从属性生成代码的过程宏
宏的第二种形式是过程宏,它的作用更像是 函数(并且是一种过程)。过程宏接受一些代码作为 input,对该代码进行作,并生成一些代码作为输出,而不是 匹配模式并将代码替换为其他声明性代码 宏可以。这三种过程宏是自定义派生的, attribute-like 和 function-like 都以类似的方式工作。
创建过程宏时,定义必须位于其自己的 crate 中
具有特殊的 crate 类型。这是出于我们希望的复杂技术原因
以在将来消除。在示例 19-29 中,我们展示了如何定义
procedural 宏,其中some_attribute
是使用特定
宏观多样性。
文件名: src/lib.rs
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
示例 19-29:定义过程的示例 宏
定义过程宏的函数采用TokenStream
作为输入
并生成一个TokenStream
作为输出。这TokenStream
type 由
这proc_macro
crate 包含在 Rust 中,表示
令 牌。这是宏的核心:宏所在的源代码
作构成输入TokenStream
和宏生成的代码
是输出TokenStream
.该函数还附加了一个属性
它指定了我们正在创建的程序宏类型。我们可以拥有
同一 crate 中的多种过程宏。
让我们看看不同类型的过程宏。我们将从一个 自定义 derive 宏,然后解释使 其他形式不同。
如何编写自定义derive
宏
让我们创建一个名为hello_macro
定义一个名为HelloMacro
替换为一个名为hello_macro
.而不是
使我们的用户实现HelloMacro
trait 的 trait 中,
我们将提供一个过程宏,以便用户可以使用#[derive(HelloMacro)]
要获取hello_macro
功能。默认实现将打印Hello, Macro! My name is TypeName!
哪里TypeName
是此特征所具有的类型的名称
被定义。换句话说,我们将编写一个 crate,为另一个
programmer 使用我们的 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();
}
但是,他们需要为每种类型编写 implementation block
想要与hello_macro
;我们想让他们不必这样做
工作。
此外,我们还无法提供hello_macro
函数替换为 default
implementation 中,该 implementation 将打印实现 trait 的类型的名称
on:Rust 没有反射功能,因此它无法查找类型的
name 的 intent 值。我们需要一个宏来在编译时生成代码。
下一步是定义过程宏。在撰写本文时,
过程宏需要位于自己的 crate 中。最终,此限制
可能会被取消。构建 crate 和宏 crate 的约定是
follows:对于名为foo
,则自定义 derive procedural macro crate 为
叫foo_derive
.让我们启动一个名为hello_macro_derive
里面
我们hello_macro
项目:
$ cargo new hello_macro_derive --lib
我们的两个 crate 密切相关,因此我们创建了 procedure 宏 crate
在我们的hello_macro
板条箱。如果我们更改 trait
定义hello_macro
,我们必须更改
程序宏hello_macro_derive
也。这两个板条箱需要
单独发布,使用这些 crate 的程序员需要添加
两者都作为依赖项,并将它们都引入范围。我们可以改用hello_macro
板条箱使用hello_macro_derive
作为依赖项重新导出
过程宏代码。然而,我们构建项目的方式使它
可供程序员使用hello_macro
即使他们不想要derive
功能性。
我们需要声明hello_macro_derive
crate 作为程序宏 crate 进行设置。
我们还需要syn
和quote
板条箱,如你所见
稍后,我们需要将它们添加为 dependencies。将以下内容添加到 Cargo.toml 文件中hello_macro_derive
:
文件名: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
要开始定义 proc 宏,请将示例 19-31 中的代码放入
您的 src/lib.rs 文件hello_macro_derive
板条箱。请注意,此代码
不会编译,直到我们为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
)对于几乎每个
Procedural Macro crate 中看到或创建。您在 的正文中指定的代码
内部函数 (impl_hello_macro
在这种情况下)将有所不同
取决于过程宏的用途。
我们推出了三个新的 crate:proc_macro
,syn
和quote
.这proc_macro
crate 是 Rust 附带的,所以我们不需要将其添加到
dependencies 中的 Cargo.toml 中。这proc_macro
crate 是编译器的 API,它
允许我们从代码中读取和作 Rust 代码。
这syn
crate 将 Rust 代码从字符串解析为数据结构,我们
可以执行作。这quote
板条箱转弯syn
数据结构返回
转换为 Rust 代码。这些 crate 使解析任何类型的 Rust 变得更加简单
我们可能想要处理的代码:为 Rust 代码编写一个完整的解析器并不简单
任务。
这hello_macro_derive
函数将在我们库的用户
指定#[derive(HelloMacro)]
在类型上。这是可能的,因为我们
注解了hello_macro_derive
函数与proc_macro_derive
和
指定了名称HelloMacro
,它与我们的特征名称匹配;这是
约定,大多数过程宏都遵循。
这hello_macro_derive
函数首先将input
从TokenStream
转换为我们可以解释和执行的数据结构
作上。这是syn
开始发挥作用。这parse
函数syn
采用TokenStream
并返回一个DeriveInput
struct 表示
解析的 Rust 代码。示例 19-32 显示了DeriveInput
struct 中,我们从解析struct 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: 该DeriveInput
实例,我们何时获得
解析示例 19-30 中具有 macro 属性的代码
这个结构体的字段显示我们解析的 Rust 代码是一个单元结构体
使用ident
(identifier,表示名称)Pancakes
.还有更多
fields 来描述各种 Rust 代码;检查syn
文档DeriveInput
了解更多信息。
很快,我们将定义impl_hello_macro
函数,这就是我们将构建的地方
我们想要包含的新 Rust 代码。但在我们这样做之前,请注意 output
因为我们的 derive 宏也是一个TokenStream
.返回的TokenStream
是
添加到我们的 crate 用户编写的代码中,因此当他们编译 crate 时,
他们将获得我们在修改后的TokenStream
.
您可能已经注意到,我们正在调用unwrap
导致hello_macro_derive
函数来 panic 中,如果调用syn::parse
功能
这里失败了。我们的过程宏有必要在错误时 panic ,因为proc_macro_derive
函数必须返回TokenStream
而不是Result
自
符合过程宏 API。我们使用unwrap
;在生产代码中,您应该提供更具体的错误消息
关于使用panic!
或expect
.
现在我们有了将带注释的 Rust 代码从TokenStream
转换为DeriveInput
实例中,让我们生成实现HelloMacro
trait 的 trait 类型,如示例 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:实现HelloMacro
trait 使用
解析后的 Rust 代码
我们得到一个Ident
struct 实例,其中包含
注解类型使用ast.ident
.示例 19-32 中的结构体显示,当
我们运行impl_hello_macro
函数,则ident
我们得到的将有ident
值为"Pancakes"
.因此
这name
示例 19-33 中的 variable 将包含一个Ident
struct 实例
打印时,它将是字符串"Pancakes"
中,结构体的名称
示例 19-30.
这quote!
macro 让我们定义要返回的 Rust 代码。这
编译器期望与quote!
macro 的执行,因此我们需要将其转换为TokenStream
.我们通过以下方式做到这一点
调用into
方法,该方法使用这个中间表示和
返回 required 的值TokenStream
类型。
这quote!
Macro 还提供了一些非常酷的模板机制:我们可以
进入#name
和quote!
会将其替换为变量name
.您甚至可以执行一些重复作,类似于常规宏的工作方式。
退房这quote
Crate 的文档进行全面的介绍。
我们希望我们的 procedural 宏生成HelloMacro
trait 来获取用户注释的类型,我们可以通过使用#name
.这
trait 实现具有 one 函数hello_macro
,其正文包含
我们希望提供的功能:打印Hello, Macro! My name is
然后
带注释类型的名称。
这stringify!
宏内置于 Rust 中。它需要一个 Rust
表达式,例如1 + 2
,并在编译时将表达式转换为
string 文本,例如"1 + 2"
.这与format!
或println!
,宏计算表达式,然后将结果转换为
一个String
.有可能#name
input 可以是
表达式来按字面打印,因此我们使用stringify!
.用stringify!
也
通过转换来保存分配#name
转换为字符串文本。
此时,cargo build
应成功完成hello_macro
和hello_macro_derive
.让我们将这些 crate 连接到 清单 中的代码
19-30 查看程序宏的实际作!在 中创建一个新的二进制项目
使用cargo new pancakes
.我们需要添加hello_macro
和hello_macro_derive
作为依赖项在pancakes
crate 的 Cargo.toml 中。如果您要发布hello_macro
和hello_macro_derive
对 crates.io 来说,它们将是常规的
依赖;如果没有,你可以将它们指定为path
dependencies 中,如下所示:
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
trait 包含在内,但没有pancakes
crate 需要实现它;这#[derive(HelloMacro)]
添加了
trait 实现。
接下来,让我们探讨一下其他类型的过程宏与自定义宏有何不同 派生宏。
类似属性的宏
类似属性的宏类似于自定义派生宏,但不是
为derive
属性,它们允许您创建新的
属性。它们也更灵活:derive
仅适用于结构体和
枚举;属性也可以应用于其他项目,例如函数。
下面是一个使用类似 attribute-like 的宏的示例:假设你有一个 attribute
叫route
,在使用 Web 应用程序框架时对函数进行注释:
#[route(GET, "/")]
fn index() {
这#[route]
属性将被框架定义为过程
宏。宏定义函数的签名如下所示:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
这里,我们有两个类型的参数TokenStream
.第一个是
contents 的GET, "/"
部分。第二个是
item 属性附加到的 item 中:在本例中为fn index() {}
其余的
函数体中。
除此之外,类似属性的宏的工作方式与自定义派生相同
宏:使用proc-macro
crate 类型并实现
函数生成您想要的代码!
类似函数的宏
类似函数的宏定义看起来像函数调用的宏。与macro_rules!
宏,它们比函数更灵活;例如,他们
可以接受未知数量的参数。然而macro_rules!
宏可以是
仅使用我们在本节中讨论的类似 match 的语法定义“声明性宏macro_rules!
通用
元编程”早些时候。类似函数的宏采用TokenStream
参数及其定义会作该TokenStream
像其他两种类型的过程宏一样使用 Rust 代码。一个
function-like 宏是一个sql!
宏,可以这样调用:
let sql = sql!(SELECT * FROM posts WHERE id=1);
这个宏会解析其中的 SQL 语句,并检查它是否是
语法正确,这比macro_rules!
macro 可以做到。这sql!
macro 的定义如下:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
此定义类似于自定义 derive 宏的签名:我们接收 括号内的标记,并返回我们想要的代码 生成。
总结
呼!现在,您的工具箱中有一些您可能不会使用的 Rust 功能 通常,但您会知道它们在非常特殊的情况下可用。 我们介绍了几个复杂的主题,以便您在 错误消息建议或其他人的代码中,您将能够 识别这些概念和语法。使用本章作为指南的参考 你到解决方案。
接下来,我们要把我们在整本书中讨论的所有内容付诸实践 再做一个项目!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准