Update ch19-06-macros.md

This commit is contained in:
Nango 2020-08-08 07:31:13 -05:00 committed by GitHub
parent 4bec93cede
commit 7c641f104d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -7,7 +7,7 @@
我们已经在本书中使用过像 `println!` 这样的宏了,不过还没完全探索什么是宏以及它是如何工作的。**宏***Macro*)指的是 Rust 中一系列的功能:**声明***Declarative*)宏,使用 `macro_rules!`,和三种 **过程***Procedural*)宏:
* 自定义 `#[derive]` 宏在结构体和枚举上指定通过 `derive` 属性添加的代码
* 类属性Attribute宏定义可用于任意项的自定义属性
* 类属性Attribute-like)宏定义可用于任意项的自定义属性
* 类函数宏看起来像函数不过作用于作为参数传递的 token。
我们会依次讨论每一种宏,不过首要的是,为什么已经有了函数还需要宏呢?
@ -16,13 +16,13 @@
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 **元编程***metaprogramming*)。在附录 C 中会探讨 `derive` 属性,其生成各种 trait 的实现。我们也在本书中使用过 `println!` 宏和 `vec!` 宏。所有的这些宏以 **展开** 的方式来生成比你所手写出的更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数的角色。但宏有一些函数所没有的附加能力。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
一个函数标签必须声明函数参数个数和类型。相比之下,宏只接受一个可变参数:用一个参数调用 `println!("hello")` 或用两个参数调用 `println!("hello {}", name)` 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
一个函数标签必须声明函数参数个数和类型。相比之下,宏能够接受不同数量的参数:用一个参数调用 `println!("hello")` 或用两个参数调用 `println!("hello {}", name)` 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
实现一个宏而不是函数的消极面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在调用宏 **之前** 必须定义并将其引入作用域,而函数则可以在任何地方定义和调用。
宏和函数的最后一个重要的区别是:在一个文件里调用宏 **之前** 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
### 使用 `macro_rules!` 的声明宏用于通用元编程
@ -62,7 +62,7 @@ macro_rules! vec {
无论何时导入定义了宏的包,`#[macro_export]` 注解说明宏应该是可用的。 如果没有该注解,这个宏不能被引入作用域。
接着使用 `macro_rules!` 和宏名称开始宏定义,且所定义的宏并 *不带* 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 `vec`
接着使用 `macro_rules!` 和宏名称开始宏定义,且所定义的宏并 **不带** 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 `vec`
`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个单边模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。假设这是这个宏中唯一的模式,则只有这一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
@ -70,13 +70,13 @@ macro_rules! vec {
[参考]: https://doc.rust-lang.org/reference/macros.html
首先,一对括号包含了全部模式。接下来是后跟一对括号的美元符号( `$` ),其通过替代代码捕获了符合括号内模式的值。`$()` 内则是 `$x:expr` ,其匹配 Rust 的任意表达式或给定 `$x` 名字的表达式
首先,一对括号包含了整个模式。接下来是美元符号( `$` ),后跟一对括号,捕获了符合括号内模式的值以用于替换后的代码。`$()` 内则是 `$x:expr` ,其匹配 Rust 的任意表达式,并将该表达式记作 `$x`
`$()` 之后的逗号说明一个逗号分隔符可以有选择的出现代码之后,这段代码与在 `$()` 中所捕获的代码相匹配。紧随逗号之后的 `*` 说明该模式匹配零个或多个 `*` 之前的任何模式。
`$()` 之后的逗号说明一个可有可无的逗号分隔符可以出现在 `$()` 所匹配的代码之后。紧随逗号之后的 `*` 说明该模式匹配零个或多个 `*` 之前的任何模式。
当以 `vec![1, 2, 3];` 调用宏时,`$x` 模式与三个表达式 `1`、`2` 和 `3` 进行了三次匹配。
现在让我们来看看这个出现在与此单边模式相关的代码块中的模式:在 `$()*` 部分中所生成的 `temp_vec.push()` 为在匹配到模式中的 `$()` 每一部分而生成。`$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样:
现在让我们来看看与此单边模式相关联的代码块中的模式:对于每个(在 `=>` 前面)匹配模式中的 `$()` 的部分,生成零个或更多个(在 `=>` 后面)位于 `$()*` 内的 `temp_vec.push()` ,生成的个数取决于该模式被匹配的次数。`$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样:
```rust,ignore
let mut temp_vec = Vec::new();
@ -96,7 +96,7 @@ temp_vec
第二种形式的宏被称为 **过程宏***procedural macros*),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
有三种类型的过程宏(自定义 derive,类属性和类函数),不过它们的工作方式都类似。
有三种类型的过程宏(自定义派生derive,类属性和类函数),不过它们的工作方式都类似。
当创建过程宏时,其定义必须位于一种特殊类型的属于它们自己的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。使用这些宏需采用类似示例 19-29 所示的代码形式,其中 `some_attribute` 是一个使用特定宏的占位符。
@ -180,7 +180,7 @@ fn main() {
$ cargo new hello_macro_derive --lib
```
由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait ,同时也必须改变在 `hello_macro_derive` 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重新导出过程式宏的代码。但我们组织项目的方式使编程人员使用 `hello_macro` 成为可能,即使他们无需 `derive` 的功能
由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait ,同时也必须改变在 `hello_macro_derive` 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重新导出过程式宏的代码。但现在我们组织项目的方式使编程人员在无需 `derive` 功能时也能够单独使用 `hello_macro`
需要将 `hello_macro_derive` 声明为一个过程宏的 crate。同时也需要 `syn``quote` crate 中的功能,正如注释中所说,需要将其加到依赖中。为 `hello_macro_derive` 将下面的代码加入到 *Cargo.toml* 文件中。
@ -238,7 +238,7 @@ pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
[`syn`]: https://crates.io/crates/syn
[`quote`]: https://crates.io/crates/quote
`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构反过来传入到 Rust 代码中。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。原因在于我们已经使用 `proc_macro_derive` 及其指定名称对 `hello_macro_derive` 函数进行了注解:`HelloMacro` ,其匹配到 trait 名,这是大多数过程宏遵循的习惯。
@ -272,7 +272,7 @@ DeriveInput {
此时,尚未定义 `impl_hello_macro` 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 `TokenStream`。所返回的 `TokenStream` 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会获取到我们所提供的额外功能。
你可能也注意到了,当调用 `parse_derive_input` 或 `parse` 失败时。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!``expect` 来提供关于发生何种错误的更加明确的错误信息。
你可能也注意到了,当调用 `syn::parse` 函数失败时,我们用 `unwrap` 来使 `hello_macro_derive` 函数 panic。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!``expect` 来提供关于发生何种错误的更加明确的错误信息。
现在我们有了将注解的 Rust 代码从 `TokenStream` 转换为 `DeriveInput` 实例的代码,让我们来创建在注解类型上实现 `HelloMacro` trait 的代码,如示例 19-33 所示。
@ -306,7 +306,7 @@ fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
此处所使用的 `stringify!` 为 Rust 内置宏。其接收一个 Rust 表达式,如 `1 + 2` 然后在编译时将表达式转换为一个字符串常量,如 `"1 + 2"` 。这与 `format!``println!` 是不同的,它计算表达式并将结果转换为 `String` 。有一种可能的情况是,所输入的 `#name` 可能是一个需要打印的表达式,因此我们用 `stringify!``stringify!` 编译时也保留了一份将 `#name` 转换为字符串之后的内存分配。
此时,`cargo build` 应该都能成功编译 `hello_macro``hello_macro_derive` 。我们将这些 crate 连接到示例 19-38 的代码中来看看过程宏的行为!在 *projects* 目录下用 `cargo new pancakes` 命令新建一个二进制项目。需要将 `hello_macro``hello_macro_derive` 作为依赖加到 `pancakes` 包的 *Cargo.toml* 文件中去。如果你正将 `hello_macro``hello_macro_derive` 的版本发布到 [crates.io](https://crates.io/) 上,其应为规依赖;如果不是,则可以像下面这样将其指定为 `path` 依赖:
此时,`cargo build` 应该都能成功编译 `hello_macro``hello_macro_derive` 。我们将这些 crate 连接到示例 19-38 的代码中来看看过程宏的行为!在 *projects* 目录下用 `cargo new pancakes` 命令新建一个二进制项目。需要将 `hello_macro``hello_macro_derive` 作为依赖加到 `pancakes` 包的 *Cargo.toml* 文件中去。如果你正将 `hello_macro``hello_macro_derive` 的版本发布到 [crates.io](https://crates.io/) 上,其应为规依赖;如果不是,则可以像下面这样将其指定为 `path` 依赖:
```toml
[dependencies]
@ -334,7 +334,7 @@ fn index() {
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
```
这里有两个 `TokenStream` 类型的参数;第一个用于属性内容本身,也就是 `GET, "/"` 部分。第二个是属性所标记的项在本例中,是 `fn index() {}` 和剩下的函数体。
这里有两个 `TokenStream` 类型的参数;第一个用于属性内容本身,也就是 `GET, "/"` 部分。第二个是属性所标记的项在本例中,是 `fn index() {}` 和剩下的函数体。
除此之外,类属性宏与自定义派生宏工作方式一致:创建 `proc-macro` crate 类型的 crate 并实现希望生成代码的函数!