mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-14 04:41:49 +08:00
Fixing translation errors in ch19-06-macros.md
Fixing multiple translation errors in ch19-06-macros.md and adjusting translations in this chapter to make it more appropriate and readble.
This commit is contained in:
parent
2a7d8b10e9
commit
ba9539f7ed
@ -18,15 +18,15 @@
|
||||
|
||||
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
|
||||
|
||||
一个函数标签必须声明函数参数个数和类型。相比之下,宏能够接受不同数量的参数:用一个参数调用 `println!("hello")` 或用两个参数调用 `println!("hello {}", name)` 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
|
||||
一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数:用一个参数调用 `println!("hello")` 或用两个参数调用 `println!("hello {}", name)` 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
|
||||
|
||||
实现一个宏而不是一个函数的缺点是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
|
||||
实现宏不如实现函数的一面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
|
||||
|
||||
宏和函数的最后一个重要的区别是:在一个文件里调用宏 **之前** 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
|
||||
|
||||
### 使用 `macro_rules!` 的声明宏用于通用元编程
|
||||
|
||||
Rust 最常用的宏形式是 **声明宏**(*declarative macros*)。它们有时也被称为 “macros by example”、“`macro_rules!` 宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust `match` 表达式的代码。正如在第六章讨论的那样,`match` 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和传递给宏的源代码进行比较,同时每个模式的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。
|
||||
Rust 最常用的宏形式是 **声明宏**(*declarative macros*)。它们有时也被称为 “macros by example”、“`macro_rules!` 宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust `match` 表达式的代码。正如在第六章讨论的那样,`match` 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和前面提到的源代码字面值进行比较,每个模式的相关代码会替换传递给宏的代码。所有这一切都发生于编译时。
|
||||
|
||||
可以使用 `macro_rules!` 来定义宏。让我们通过查看 `vec!` 宏定义来探索如何使用 `macro_rules!` 结构。第八章讲述了如何使用 `vec!` 宏来生成一个给定值的 vector。例如,下面的宏用三个整数创建一个 vector:
|
||||
|
||||
@ -49,23 +49,23 @@ let v: Vec<u32> = vec![1, 2, 3];
|
||||
> 注意:标准库中实际定义的 `vec!` 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。
|
||||
|
||||
|
||||
无论何时导入定义了宏的包,`#[macro_export]` 注解说明宏应该是可用的。 如果没有该注解,这个宏不能被引入作用域。
|
||||
`#[macro_export]` 注解表明只要导入了定义这个宏的crate,该宏就应该是可用的。 如果没有该注解,这个宏不能被引入作用域。
|
||||
|
||||
接着使用 `macro_rules!` 和宏名称开始宏定义,且所定义的宏并 **不带** 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 `vec` 。
|
||||
|
||||
`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个单边模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。假设这是这个宏中唯一的模式,则只有这一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
|
||||
`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个单边模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。这里这个宏只有一个模式,那就只有一个有效匹配方向,其他任何模式方向(译者注:不匹配这个模式)都会导致错误。更复杂的宏会有多个分支模式。
|
||||
|
||||
宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅[参考]。
|
||||
|
||||
[参考]: https://doc.rust-lang.org/reference/macros-by-example.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)`,生成零次还是多次取决于模式匹配到多少次。`$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样:
|
||||
|
||||
```rust,ignore
|
||||
{
|
||||
@ -103,7 +103,7 @@ pub fn some_name(input: TokenStream) -> TokenStream {
|
||||
|
||||
<span class="caption">示例 19-29: 一个使用过程宏的例子</span>
|
||||
|
||||
定义过程宏的函数以 TokenStream 作为输入并生成 TokenStream 作为输出。 TokenStream 类型由包含在 Rust 中的 proc_macro crate 定义并表示令牌序列。 这是宏的核心:宏所操作的源代码构成了输入 TokenStream,宏产生的代码是输出 TokenStream。 该函数还附加了一个属性,用于指定我们正在创建的程序宏类型。 我们可以在同一个 crate 中拥有多种程序宏。
|
||||
定义过程宏的函数接收一个 TokenStream 作为输入并生成 TokenStream 作为输出。`TokenStream` 是定义于`proc_macro` crate里代表一系列token的类型,Rust默认携带了`proc_macro` crate。 这就是宏的核心:宏所处理的源代码组成了输入 `TokenStream`,宏生成的代码是输出 `TokenStream`。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一 crate 中可以有多种的过程宏。
|
||||
|
||||
让我们看看不同种类的程序宏。 我们将从一个自定义的派生宏开始,然后解释使其他形式不同的小差异。
|
||||
|
||||
@ -151,7 +151,7 @@ $ cargo new hello_macro_derive --lib
|
||||
|
||||
由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait ,同时也必须改变在 `hello_macro_derive` 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重新导出过程式宏的代码。但现在我们组织项目的方式使编程人员在无需 `derive` 功能时也能够单独使用 `hello_macro`。
|
||||
|
||||
我们需要声明 `hello_macro_derive` crate 是过程宏(proc-macro) crate。正如稍后将看到的那样,我们还需要 `syn` 和 `quote` crate 中的功能,所以需要将其加到依赖中。将下面的代码加入到 `hello_macro_derive` 的 *Cargo.toml* 文件中。
|
||||
我们需要声明 `hello_macro_derive` crate 是过程宏(proc-macro) crate。我们还需要 `syn` 和 `quote` crate 中的功能,正如你即将看到的,需要将他们加到依赖中。将下面的代码加入到 `hello_macro_derive` 的 *Cargo.toml* 文件中。
|
||||
|
||||
<span class="filename">文件名: hello_macro_derive/Cargo.toml</span>
|
||||
|
||||
@ -169,7 +169,7 @@ $ cargo new hello_macro_derive --lib
|
||||
|
||||
<span class="caption">示例 19-31: 大多数过程式宏处理 Rust 代码时所需的代码</span>
|
||||
|
||||
注意 `hello_macro_derive` 函数中代码分割的方式,它负责解析 `TokenStream`,而 `impl_hello_macro` 函数则负责转换语法树:这让编写一个过程式宏更加方便。外部函数中的代码(在这里是 `hello_macro_derive`)几乎在所有你能看到或创建的过程宏 crate 中都一样。内部函数(在这里是 `impl_hello_macro`)的函数体中所指定的代码则依过程宏的目的而各有不同。
|
||||
注意我们将代码分成了`hello_macro_derive` 和 `impl_macro_derive` 两个函数,前者负责解析 `TokenStream`,后者负责转换语法树:这使得编写过程宏更方便。几乎你看到或者创建的每一个过程宏的外部函数(这里是`hello_macro_derive`)中的代码都跟这里是一样的。你放入内部函数(这里是`impl_macro_derive`)中的代码根据你的过程宏的设计目的会有所不同。
|
||||
|
||||
现在,我们已经引入了三个新的 crate:`proc_macro` 、 [`syn`] 和 [`quote`] 。Rust 自带 `proc_macro` crate,因此无需将其加到 *Cargo.toml* 文件的依赖中。`proc_macro` crate 是编译器用来读取和操作我们 Rust 代码的 API。
|
||||
|
||||
@ -178,7 +178,7 @@ $ cargo new hello_macro_derive --lib
|
||||
|
||||
`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
|
||||
|
||||
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。原因在于我们已经使用 `proc_macro_derive` 及其指定名称对 `hello_macro_derive` 函数进行了注解:`HelloMacro` ,其匹配到 trait 名,这是大多数过程宏遵循的习惯。
|
||||
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。因为我们已经使用 `proc_macro_derive` 及其指定名称`HelloMacro`对 `hello_macro_derive` 函数进行了注解,指定名称`HelloMacro`就是 trait 名,这是大多数过程宏遵循的习惯。
|
||||
|
||||
该函数首先将来自 `TokenStream` 的 `input` 转换为一个我们可以解释和操作的数据结构。这正是 `syn` 派上用场的地方。`syn` 中的 `parse_derive_input` 函数获取一个 `TokenStream` 并返回一个表示解析出 Rust 代码的 `DeriveInput` 结构体。示例 19-32 展示了从字符串 `struct Pancakes;` 中解析出来的 `DeriveInput` 结构体的相关部分:
|
||||
|
||||
@ -208,7 +208,7 @@ DeriveInput {
|
||||
|
||||
[syn-docs]: https://docs.rs/syn/0.14.4/syn/struct.DeriveInput.html
|
||||
|
||||
此时,尚未定义 `impl_hello_macro` 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 `TokenStream`。所返回的 `TokenStream` 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会获取到我们所提供的额外功能。
|
||||
很快我们将定义 `impl_hello_macro` 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 `TokenStream`。所返回的 `TokenStream` 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会通过修改后的 `TokenStream` 获取到我们所提供的额外功能。
|
||||
|
||||
你可能也注意到了,当调用 `syn::parse` 函数失败时,我们用 `unwrap` 来使 `hello_macro_derive` 函数 panic。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!` 或 `expect` 来提供关于发生何种错误的更加明确的错误信息。
|
||||
|
||||
@ -224,7 +224,7 @@ DeriveInput {
|
||||
|
||||
我们得到一个包含以 `ast.ident` 作为注解类型名字(标识符)的 `Ident` 结构体实例。示例 19-32 中的结构体表明当 `impl_hello_macro` 函数运行于示例 19-30 中的代码上时 `ident` 字段的值是 `"Pancakes"`。因此,示例 19-33 中 `name` 变量会包含一个 `Ident` 结构体的实例,当打印时,会是字符串 `"Pancakes"`,也就是示例 19-30 中结构体的名称。
|
||||
|
||||
`quote!` 宏让我们可以编写希望返回的 Rust 代码。`quote!` 宏执行的直接结果并不是编译器所期望的并需要转换为 `TokenStream`。为此需要调用 `into` 方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 `TokenStream` 类型值。
|
||||
`quote!` 宏能让我们编写希望返回的 Rust 代码。`quote!` 宏执行的直接结果并不是编译器所期望的所以需要转换为 `TokenStream`。为此需要调用 `into` 方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 `TokenStream` 类型值。
|
||||
|
||||
这个宏也提供了一些非常酷的模板机制;我们可以写 `#name` ,然后 `quote!` 会以名为 `name` 的变量值来替换它。你甚至可以做一些类似常用宏那样的重复代码的工作。查阅 [`quote` crate 的文档][quote-docs] 来获取详尽的介绍。
|
||||
|
||||
@ -232,7 +232,7 @@ DeriveInput {
|
||||
|
||||
我们期望我们的过程式宏能够为通过 `#name` 获取到的用户注解类型生成 `HelloMacro` trait 的实现。该 trait 的实现有一个函数 `hello_macro` ,其函数体包括了我们期望提供的功能:打印 `Hello, Macro! My name is` 和注解的类型名。
|
||||
|
||||
此处所使用的 `stringify!` 为 Rust 内置宏。其接收一个 Rust 表达式,如 `1 + 2` , 然后在编译时将表达式转换为一个字符串常量,如 `"1 + 2"` 。这与 `format!` 或 `println!` 是不同的,它计算表达式并将结果转换为 `String` 。有一种可能的情况是,所输入的 `#name` 可能是一个需要打印的表达式,因此我们用 `stringify!` 。 `stringify!` 编译时也保留了一份将 `#name` 转换为字符串之后的内存分配。
|
||||
此处所使用的 `stringify!` 为 Rust 内置宏。其接收一个 Rust 表达式,如 `1 + 2` , 然后在编译时将表达式转换为一个字符串常量,如 `"1 + 2"` 。这与 `format!` 或 `println!` 是不同的,它计算表达式并将结果转换为 `String` 。有一种可能的情况是,所输入的 `#name` 可能是一个需要打印的表达式,因此我们用 `stringify!` 。`stringify!` 也能通过在编译时将 `#name` 转换为字符串来节省内存分配。
|
||||
|
||||
此时,`cargo build` 应该都能成功编译 `hello_macro` 和 `hello_macro_derive` 。我们将这些 crate 连接到示例 19-30 的代码中来看看过程宏的行为!在 *projects* 目录下用 `cargo new pancakes` 命令新建一个二进制项目。需要将 `hello_macro` 和 `hello_macro_derive` 作为依赖加到 `pancakes` 包的 *Cargo.toml* 文件中去。如果你正将 `hello_macro` 和 `hello_macro_derive` 的版本发布到 [crates.io](https://crates.io/) 上,其应为常规依赖;如果不是,则可以像下面这样将其指定为 `path` 依赖:
|
||||
|
||||
@ -246,7 +246,7 @@ DeriveInput {
|
||||
|
||||
### 类属性宏
|
||||
|
||||
类属性宏与自定义派生宏相似,不同于为 `derive` 属性生成代码,它们允许你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于注解 web 应用程序框架(web application framework)的函数:
|
||||
类属性宏与自定义派生宏相似,不同的是 `derive` 属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于注解 web 应用程序框架(web application framework)的函数:
|
||||
|
||||
```rust,ignore
|
||||
#[route(GET, "/")]
|
||||
@ -266,7 +266,7 @@ pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
|
||||
### 类函数宏
|
||||
|
||||
类函数宏定义看起来像函数调用的宏。类似于 `macro_rules!`,它们比函数更灵活;例如,可以接受未知数量的参数。然而 `macro_rules!` 宏只能使用之前 [“使用 `macro_rules!` 的声明宏用于通用元编程”][decl] 介绍的类匹配的语法定义。类函数宏获取 `TokenStream` 参数,其定义使用 Rust 代码操纵 `TokenStream`,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的 `sql!` 宏:
|
||||
类函数(Function-like)宏的定义看起来像函数调用的宏。类似于 `macro_rules!`,它们比函数更灵活;例如,可以接受未知数量的参数。然而 `macro_rules!` 宏只能使用之前 [“使用 `macro_rules!` 的声明宏用于通用元编程”][decl] 介绍的类匹配的语法定义。类函数宏获取 `TokenStream` 参数,其定义使用 Rust 代码操纵 `TokenStream`,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的 `sql!` 宏:
|
||||
|
||||
[decl]: #使用-macro_rules-的声明宏用于通用元编程
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user