Fix typo in macro.md

pull/439/head
lijinpeng 3 years ago
parent 2bf79951b9
commit 413eac7e17

@ -16,13 +16,13 @@ fn main() {
虽然三种使用形式皆可,但是 Rust 内置的宏都有自己约定俗成的使用方式,例如 `vec![...]`、`assert_eq!(...)` 等。 虽然三种使用形式皆可,但是 Rust 内置的宏都有自己约定俗成的使用方式,例如 `vec![...]`、`assert_eq!(...)` 等。
在 Rust 中宏分为两大类:声明式宏 `macro_rules!` 和三种过程宏( *procedural macros* ): 在 Rust 中宏分为两大类:**声明式宏( *declarative macros* )** `macro_rules!` 和三种**过程宏( *procedural macros* )**:
- `#[derive]`,在之前多次见到的派生宏,可以为目标结构体或枚举派生指定的代码,例如 `Debug` 特征 - `#[derive]`,在之前多次见到的派生宏,可以为目标结构体或枚举派生指定的代码,例如 `Debug` 特征
- 类属性宏(Attribute-like macro),用于为目标添加自定义的属性 - 类属性宏(Attribute-like macro),用于为目标添加自定义的属性
- 类函数宏(Function-like macro),看上去就像是函数调用 - 类函数宏(Function-like macro),看上去就像是函数调用
如果感觉难以理解,也不必担心,接下来我们将逐个看看它们的庐山真面目,在之前,先来看下为何需要宏,特别是 Rust 的函数明明已经很强大了。 如果感觉难以理解,也不必担心,接下来我们将逐个看看它们的庐山真面目,在之前,先来看下为何需要宏,特别是 Rust 的函数明明已经很强大了。
## 宏和函数的区别 ## 宏和函数的区别
宏和函数的区别并不少,而且对于宏擅长的领域,函数其实是有些无能为力的。 宏和函数的区别并不少,而且对于宏擅长的领域,函数其实是有些无能为力的。
@ -30,7 +30,7 @@ fn main() {
#### 元编程 #### 元编程
从根本上来说,宏是通过一种代码来生成另一种代码,如果大家熟悉元编程,就会发现两者的共同点。 从根本上来说,宏是通过一种代码来生成另一种代码,如果大家熟悉元编程,就会发现两者的共同点。
在[附录 D](https://course.rs/appendix/derive.html)讲到的 `derive` 属性,就会自动为结构体派生出相应特征所需的代码,例如 `#[derive(Debug)]`,还有熟悉的 `println!``vec!`,所有的这些宏都会展开成相应的代码,且很可能是长得多的代码。 在[附录 D](https://course.rs/appendix/derive.html)讲到的 `derive` 属性,就会自动为结构体派生出相应特征所需的代码,例如 `#[derive(Debug)]`,还有熟悉的 `println!``vec!`,所有的这些宏都会展开成相应的代码,且很可能是长得多的代码。
总之,元编程可以帮我们减少所需编写的代码,也可以一定程度上减少维护的成本,虽然函数复用也有类似的作用,但是宏依然拥有自己独特的优势。 总之,元编程可以帮我们减少所需编写的代码,也可以一定程度上减少维护的成本,虽然函数复用也有类似的作用,但是宏依然拥有自己独特的优势。
@ -113,7 +113,7 @@ macro_rules! vec {
1. `$()` 中包含的是模式 `$x:expr`,该模式中的 `expr` 表示会匹配任何 Rust 表达式,并给予该模式一个名称 `$x` 1. `$()` 中包含的是模式 `$x:expr`,该模式中的 `expr` 表示会匹配任何 Rust 表达式,并给予该模式一个名称 `$x`
2. 因此 `$x` 模式可以跟整数 `1` 进行匹配,也可以跟字符串 "hello" 进行匹配: `vec!["hello", "world"]` 2. 因此 `$x` 模式可以跟整数 `1` 进行匹配,也可以跟字符串 "hello" 进行匹配: `vec!["hello", "world"]`
3. `$()` 之后的逗号,意味着`1` 和 `2` 之间可以使用逗号进行分割,也意味着 `3` 既可以没有逗号,也可以有逗号:`vec![1, 2, 3,]` 3. `$()` 之后的逗号,意味着`1` 和 `2` 之间可以使用逗号进行分割,也意味着 `3` 既可以没有逗号,也可以有逗号:`vec![1, 2, 3,]`
4. `*` 说明之前的模式可以出现次也可以任意次,这里出现了三次 4. `*` 说明之前的模式可以出现次也可以任意次,这里出现了三次
接下来,我们再来看看与模式相关联、在 `=>` 之后的代码: 接下来,我们再来看看与模式相关联、在 `=>` 之后的代码:
```rust ```rust
@ -158,7 +158,7 @@ let v = {
由于绝大多数 Rust 开发者都是宏的用户而不是编写者,因此在这里我们不会对 `macro_rules` 进行更深入的学习,如果大家感兴趣,可以看看这本书 [ “The Little Book of Rust Macros”](https://veykril.github.io/tlborm/)。 由于绝大多数 Rust 开发者都是宏的用户而不是编写者,因此在这里我们不会对 `macro_rules` 进行更深入的学习,如果大家感兴趣,可以看看这本书 [ “The Little Book of Rust Macros”](https://veykril.github.io/tlborm/)。
## 用过程宏为属性标记生成代码 ## 用过程宏为属性标记生成代码
第二种常用的宏就是[*过程宏*]( *procedural macros* ),从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。**注意,过程宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同!** 第二种常用的宏就是[*过程宏*](https://doc.rust-lang.org/reference/procedural-macros.html) ( *procedural macros* ),从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。**注意,过程宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同!**
至于前文提到的过程宏的三种类型(自定义 `derive`、属性宏、函数宏),它们的工作方式都是类似的。 至于前文提到的过程宏的三种类型(自定义 `derive`、属性宏、函数宏),它们的工作方式都是类似的。
@ -282,7 +282,7 @@ hello_macro
由于过程宏所在的包跟我们的项目紧密相连,因此将它放在项目之中。现在,问题又来了,该如何在项目的 `src/main.rs` 中引用 `hello_macro_derive` 包的内容? 由于过程宏所在的包跟我们的项目紧密相连,因此将它放在项目之中。现在,问题又来了,该如何在项目的 `src/main.rs` 中引用 `hello_macro_derive` 包的内容?
方法有两种,第一种是将 `hello_macro_derive` 发布到 `crates.io``github` 中,就像我们引用的其它依赖一样;另一种就是使用相对路径引入的本地化方式,修改 `hello_macro/Cargo.tom` 文件添加以下内容: 方法有两种,第一种是将 `hello_macro_derive` 发布到 `crates.io``github` 中,就像我们引用的其它依赖一样;另一种就是使用相对路径引入的本地化方式,修改 `hello_macro/Cargo.toml` 文件添加以下内容:
```toml ```toml
[dependencies] [dependencies]
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" } hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
@ -334,9 +334,9 @@ pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
由于我们为 `hello_macro_derive` 函数标记了 `#[proc_macro_derive(HelloMacro)]`,当用户使用 `#[derive(HelloMacro)]` 标记了他的类型后,`hello_macro_derive` 函数就将被调用。这里的秘诀就是特征名 `HelloMacro`,它就像一座桥梁,将用户的类型和过程宏联系在一起。 由于我们为 `hello_macro_derive` 函数标记了 `#[proc_macro_derive(HelloMacro)]`,当用户使用 `#[derive(HelloMacro)]` 标记了他的类型后,`hello_macro_derive` 函数就将被调用。这里的秘诀就是特征名 `HelloMacro`,它就像一座桥梁,将用户的类型和过程宏联系在一起。
`sync` 将字符串形式的 Rust 代码解析为一个 AST 树的数据结构,该数据结构可以在随后的 `impl_hello_macro` 函数中进行操作。最后,操作的结果又会被 `quote` 包转换回 Rust 代码。这些包非常关键,可以帮我们节省大量的精力,否则你需要自己去编写支持代码解析和还原的解析器,这可不是一件简单的任务! `syn` 将字符串形式的 Rust 代码解析为一个 AST 树的数据结构,该数据结构可以在随后的 `impl_hello_macro` 函数中进行操作。最后,操作的结果又会被 `quote` 包转换回 Rust 代码。这些包非常关键,可以帮我们节省大量的精力,否则你需要自己去编写支持代码解析和还原的解析器,这可不是一件简单的任务!
`sync.parse` 调用会返回一个 `DeriveInput` 结构体来代表解析后的 Rust 代码: `syn::parse` 调用会返回一个 `DeriveInput` 结构体来代表解析后的 Rust 代码:
```rust ```rust
DeriveInput { DeriveInput {
// --snip-- // --snip--
@ -362,7 +362,7 @@ DeriveInput {
- `fields: Unit` 说明源代码是一个单元结构体 - `fields: Unit` 说明源代码是一个单元结构体
- `ident: "Sunfei"` 说明类型名称为 `Sunfei` `ident` 是标识符 `identifier` 的简写 - `ident: "Sunfei"` 说明类型名称为 `Sunfei` `ident` 是标识符 `identifier` 的简写
如果想要了解更多的信息,可以查看 [`sync` 文档](https://docs.rs/syn/1.0/syn/struct.DeriveInput.html)。 如果想要了解更多的信息,可以查看 [`syn` 文档](https://docs.rs/syn/1.0/syn/struct.DeriveInput.html)。
大家可能会注意到在 `hello_macro_derive` 函数中有 `unwrap` 的调用,也许会以为这是为了演示目的,没有做错误处理,实际上并不是的。由于该函数只能返回 `TokenStream` 而不是 `Result`,那么在报错时直接 `panic` 来抛出错误就成了相当好的选择。当然,这里实际上还是做了简化,在生产项目中,你应该通过 `panic!``expect` 抛出更具体的报错信息。 大家可能会注意到在 `hello_macro_derive` 函数中有 `unwrap` 的调用,也许会以为这是为了演示目的,没有做错误处理,实际上并不是的。由于该函数只能返回 `TokenStream` 而不是 `Result`,那么在报错时直接 `panic` 来抛出错误就成了相当好的选择。当然,这里实际上还是做了简化,在生产项目中,你应该通过 `panic!``expect` 抛出更具体的报错信息。
@ -389,7 +389,7 @@ fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
特征的 `hell_macro()` 函数只有一个功能,就是使用 `println!` 打印一行欢迎语句。 特征的 `hell_macro()` 函数只有一个功能,就是使用 `println!` 打印一行欢迎语句。
其中 `stringify!` 是 Rust 提供的内置宏,可以将一个表达式(例如 `1 + 2`)在编译期转换成一个字符串字面值(`"1 + 2"`),该字面量会直接打包进编译出的二进制文件中,具有 `'static` 生命周期。而 `format!`对表达式进行求值,最终结果是一个 `String` 类型。在这里使用 `stringify!` 有两个好处: 其中 `stringify!` 是 Rust 提供的内置宏,可以将一个表达式(例如 `1 + 2`)在编译期转换成一个字符串字面值(`"1 + 2"`),该字面量会直接打包进编译出的二进制文件中,具有 `'static` 生命周期。而 `format!`对表达式进行求值,最终结果是一个 `String` 类型。在这里使用 `stringify!` 有两个好处:
- `#name` 可能是一个表达式,我们需要它的字面值形式 - `#name` 可能是一个表达式,我们需要它的字面值形式
- 可以减少一次 `String` 带来的内存分配 - 可以减少一次 `String` 带来的内存分配

Loading…
Cancel
Save