Compare commits

...

3 Commits

Author SHA1 Message Date
KaiserY
527b61c76e update ch20-05 2025-05-11 23:37:40 +08:00
KaiserY
085552c3e8 wip: 2024 edition 2025-05-11 23:35:49 +08:00
KaiserY
55e64f884e update ch20-05 2025-05-11 21:53:18 +08:00
7 changed files with 176 additions and 111 deletions

View File

@ -21,6 +21,7 @@ edit-url-template = "https://github.com/KaiserY/trpl-zh-cn/edit/main/{path}"
[output.typst-pdf]
pdf = true
section-number = true
rust-book = true
[rust]
edition = "2021"

View File

@ -42,4 +42,26 @@ body.ayu .not_desired_behavior {
.ferris-explain {
width: 100px;
}
}
/*
A bit of a hack to make small Ferris use the existing buttons container but
only show/hide the buttons on hover over the `pre`. Targeting `.listing`
increases the specificity of this rule.
*/
pre > .buttons {
visibility: visible;
opacity: 1;
transition: none;
}
pre > .buttons button {
visibility: hidden;
opacity: 0;
transition: visibility 0.1s linear, opacity 0.1s linear;
}
pre:hover > .buttons button {
visibility: visible;
opacity: 1;
}

113
ferris.js
View File

@ -1,65 +1,102 @@
var ferrisTypes = [
// @ts-check
/**
* @typedef {{ attr: string, title: string }} FerrisType
*/
/** @type {Array<FerrisType>} */
const FERRIS_TYPES = [
{
attr: 'does_not_compile',
title: '这段代码无法通过编译!'
attr: "does_not_compile",
title: "This code does not compile!",
},
{
attr: 'panics',
title: '这段代码会 Panic'
attr: "panics",
title: "This code panics!",
},
{
attr: 'not_desired_behavior',
title: '这段代码的运行结果不符合预期。'
}
]
attr: "not_desired_behavior",
title: "This code does not produce the desired behavior.",
},
];
document.addEventListener('DOMContentLoaded', () => {
for (var ferrisType of ferrisTypes) {
attachFerrises(ferrisType)
document.addEventListener("DOMContentLoaded", () => {
for (let ferrisType of FERRIS_TYPES) {
attachFerrises(ferrisType);
}
})
});
/**
* @param {FerrisType} type
*/
function attachFerrises(type) {
var elements = document.getElementsByClassName(type.attr)
let elements = document.getElementsByClassName(type.attr);
for (var codeBlock of elements) {
var lines = codeBlock.innerText.replace(/\n$/, '').split(/\n/).length
var size = 'large'
if (lines < 4) {
size = 'small'
for (let codeBlock of elements) {
// Skip SVG etc.: in principle, these should never be attached to those, but
// this means if someone happens to have a browser extension which *is*
// attaching them, it will not break the code.
if (!(codeBlock instanceof HTMLElement)) {
continue;
}
var container = prepareFerrisContainer(codeBlock, size == 'small')
container.appendChild(createFerris(type, size))
let codeLines = codeBlock.innerText;
let extra = codeLines.endsWith("\n") ? 1 : 0;
let numLines = codeLines.split("\n").length - extra;
/** @type {'small' | 'large'} */
let size = numLines < 4 ? "small" : "large";
let container = prepareFerrisContainer(codeBlock, size == "small");
if (!container) {
continue;
}
container.appendChild(createFerris(type, size));
}
}
/**
* @param {HTMLElement} element - Code block element to attach a Ferris to.
* @param {boolean} useButtons - Whether to attach to existing buttons.
* @returns {Element | null} - The container element to use.
*/
function prepareFerrisContainer(element, useButtons) {
var foundButtons = element.parentElement.querySelector('.buttons')
let foundButtons = element.parentElement?.querySelector(".buttons");
if (useButtons && foundButtons) {
return foundButtons
return foundButtons;
}
var div = document.createElement('div')
div.classList.add('ferris-container')
let div = document.createElement("div");
div.classList.add("ferris-container");
element.parentElement.insertBefore(div, element)
if (!element.parentElement) {
console.error(`Could not install Ferris on ${element}, which is missing a parent`);
return null;
}
return div
element.parentElement.insertBefore(div, element);
return div;
}
/**
* @param {FerrisType} type
* @param {'small' | 'large'} size
* @returns {HTMLAnchorElement} - The generated anchor element.
*/
function createFerris(type, size) {
var a = document.createElement('a')
a.setAttribute('href', 'ch00-00-introduction.html#ferris')
a.setAttribute('target', '_blank')
let a = document.createElement("a");
a.setAttribute("href", "ch00-00-introduction.html#ferris");
a.setAttribute("target", "_blank");
var img = document.createElement('img')
img.setAttribute('src', 'img/ferris/' + type.attr + '.svg')
img.setAttribute('title', type.title)
img.classList.add('ferris')
img.classList.add('ferris-' + size)
let img = document.createElement("img");
img.setAttribute("src", "img/ferris/" + type.attr + ".svg");
img.setAttribute("title", type.title);
img.classList.add("ferris");
img.classList.add("ferris-" + size);
a.appendChild(img)
a.appendChild(img);
return a
}
return a;
}

View File

@ -2,33 +2,33 @@
> [ch20-05-macros.md](https://github.com/rust-lang/book/blob/main/src/ch20-05-macros.md)
> <br>
> commit 95e931170404cb98d476b19017cbbdbc00d0834d
> commit 1d1424ba1c30b8efab636c911be0a215df305eea
我们已经在本书中使用过像 `println!` 这样的宏了,不过还没完全探索什么是宏以及它是如何工作的。**宏***Macro*)指的是 Rust 中一系列的功能:使用 `macro_rules!`**声明***Declarative*)宏,和三种 **过程***Procedural*)宏
我们已经在本书中使用过像 `println!` 这样的宏了,不过尚未深入探讨什么是宏以及它是如何工作的。**宏***Macro*)指的是 Rust 中一系列的功能:使用 `macro_rules!`**声明宏***declarative macro*),和三种 **过程宏***procedural macro*
* 自定义 `#[derive]` 宏,用于在结构体和枚举上通过添加 `derive` 属性生成代码
* 类属性宏,定义可用于任意项的自定义属性
* 类函数宏,看起来像函数,但操作的是作为其参数传递的 token
我们会依次讨论每一种宏,不过首要的是,为什么已经有了函数还需要宏呢?
我们会依次讨论每一种宏,不过首要的是,让我们看看为什么已经有了函数还需要宏呢?
### 宏和函数的区别
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 **元编程***metaprogramming*)。在附录 C 中会探讨 `derive` 属性,其生成各种 trait 的实现。我们也在本书中使用 `println!` 宏和 `vec!` 宏。所有的这些宏以 **展开** 的方式来生成比你所手写出的更多的代码。
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 **元编程***metaprogramming*)。在附录 C 中会探讨 `derive` 属性,其生成各种 trait 的实现。我们也在本书中一直使用 `println!` 宏和 `vec!` 宏。所有的这些宏以 **展开** 的方式来生成比你所手写出的更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数:用一个参数调用 `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
可以使用 `macro_rules!` 来定义宏。让我们通过查看 `vec!` 宏定义来探索如何使用 `macro_rules!` 结构。第八章讲述了如何使用 `vec!` 宏来生成一个给定值的 vector。例如下面的宏用三个整数创建一个 vector
```rust
let v: Vec<u32> = vec![1, 2, 3];
@ -36,34 +36,33 @@ let v: Vec<u32> = vec![1, 2, 3];
也可以使用 `vec!` 宏来构造两个整数的 vector 或五个字符串 slice 的 vector。但却无法使用函数做相同的事情因为我们无法预先知道参数值的数量和类型。
在示例 19-28 中展示了一个 `vec!` 稍微简化的定义。
在示例 20-35 中展示了 `vec!` 宏的一个稍微简化的定义。
<span class="filename">文件名src/lib.rs</span>
```rust,noplayground
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-28/src/lib.rs}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-35/src/lib.rs}}
```
<span class="caption">示例 19-28: 一个 `vec!` 宏定义的简化版本</span>
> 注意:标准库中实际定义的 `vec!` 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。
<span class="caption">示例 20-35: 一个 `vec!` 宏定义的简化版本</span>
> 注意:标准库中实际定义的 `vec!` 包括预分配适正确数量内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。
`#[macro_export]` 注解表明只要导入了定义这个宏的 crate该宏就应该是可用的。如果没有该注解这个宏不能被引入作用域。
接着使用 `macro_rules!` 和宏名称开始宏定义,且所定义的宏并 **不带** 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 `vec`
`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个分支模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。这里这个宏只有一个模式,那就只有一个有效匹配方向,其他任何模式方向(译者注:不匹配这个模式)都会导致错误。更复杂的宏会有多个分支模式
`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个分支模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被展开。鉴于这个宏只有一个模式,那就只有一个有效匹配方式,其他任何模式方向(译者注:不匹配这个模式)都会导致错误。更复杂的宏会有不止一个分支
宏定义中有效模式语法和在第十九章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅 [Rust 参考][ref]。
宏定义中有效模式语法和在第十九章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 20-29 中模式片段什么意思。有关完整的宏模式语法,请查阅 [Rust 参考][ref]。
首先,一对括号包含了整个模式。我们使用美元符号(`$`)在宏系统中声明一个变量来包含匹配该模式的 Rust 代码。美元符号明确表明这是一个宏变量而不是普通 Rust 变量。之后是一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。`$()` 内则是 `$x:expr` ,其匹配 Rust 的任意表达式,并将该表达式命名为 `$x`
`$()` 之后的逗号说明一个可有可无的逗号分隔符可以出现在 `$()` 所匹配的代码之后。紧随逗号之后的 `*` 说明该模式匹配零个或更多个 `*` 之前的任何模式。
`$()` 之后的逗号表示在每个与 `$()` 内代码匹配的实例之间必须出现一个字面量逗号分隔符。紧随逗号之后的 `*` 说明该模式匹配零个或更多个 `*` 之前的任何模式。
当以 `vec![1, 2, 3];` 调用宏时,`$x` 模式与三个表达式 `1`、`2` 和 `3` 进行了三次匹配。
当以 `vec![1, 2, 3];` 调用宏时,`$x` 模式与三个表达式 `1`、`2` 和 `3` 对应进行了三次匹配。
现在让我们来看看与此分支模式相关联的代码块中的模式:匹配到模式中的`$()`的每一部分,都会在(`=>`右侧)`$()*` 里生成`temp_vec.push($x)`,生成零次还是多次取决于模式匹配到多少次。`$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样:
现在让我们来看看与此分支模式相关联的代码块中的模式:`$()*` 部分,`temp_vec.push($x)` 会针对模式中每次匹配到 `$()` 的部分,生成零次或多次,取决于模式匹配到多少次。`$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样:
```rust,ignore
{
@ -77,13 +76,13 @@ let v: Vec<u32> = vec![1, 2, 3];
我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的 vector 的代码。
请查阅在线文档或其他资源,如 [“The Little Book of Rust Macros”][tlborm] 来更多地了解如何写宏,该书由 Daniel Keep 开始编写并由 Lukas Wirth 继续维护
要了解更多关于如何编写宏的信息,请查阅在线文档或其他资源,如由 Daniel Keep 发起、Lukas Wirth 继续维护的 [“The Little Book of Rust Macros”][tlborm]。
### 用于从属性生成代码的过程宏
第二种形式的宏被称为 **过程宏***procedural macros*),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏自定义派生derive类属性和类函数不过它们的工作方式都类似。
第二种形式的宏被称为 **过程宏***procedural macros*),因为它们更像函数(一种类型的过程)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏自定义派生derive类属性和类函数它们的工作原理都类似。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。在示例 19-29 中展示了如何定义过程宏,其中 `some_attribute` 是一个使用特定宏变体的占位符。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于一些复杂的技术原因,将来我们希望能够消除这些限制。在示例 20-36 中展示了如何定义过程宏,其中 `some_attribute` 是一个使用特定宏变体的占位符。
<span class="filename">文件名src/lib.rs</span>
@ -95,23 +94,23 @@ pub fn some_name(input: TokenStream) -> TokenStream {
}
```
<span class="caption">示例 19-29: 一个定义过程宏的例子</span>
<span class="caption">示例 20-36: 一个定义过程宏的例子</span>
定义过程宏的函数接收一个 TokenStream 作为输入并生成 TokenStream 作为输出。`TokenStream` 是定义于`proc_macro` crate 里代表一系列 token 的类型Rust 默认携带了`proc_macro` crate。这就是宏的核心宏所处理的源代码组成了输入 `TokenStream`,宏生成的代码是输出 `TokenStream`。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一 crate 中可以有多种的过程宏。
定义过程宏的函数接收一个 `TokenStream` 作为输入并生成 `TokenStream` 作为输出。`TokenStream` 是定义于 `proc_macro` crate 里代表一系列 token 的类型Rust 默认携带了`proc_macro` crate。这就是宏的核心宏所处理的源代码组成了输入 `TokenStream`,宏生成的代码是输出 `TokenStream`。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一 crate 中可以有多种的过程宏。
让我们看看不同种类的程序宏。我们将从一个自定义的派生宏开始,然后解释使其他形式不同的小差异。
### 如何编写自定义 `derive`
让我们创建一个 `hello_macro` crate其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让用户为其每一个类型实现 `HelloMacro` trait我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解它们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate使程序员能够写类似示例 19-30 中的代码。
让我们创建一个 `hello_macro` crate其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让用户为其每一个类型实现 `HelloMacro` trait我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解它们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate使程序员能够写类似示例 20-37 中的代码。
<span class="filename">文件名src/main.rs</span>
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-30/src/main.rs}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-37/src/main.rs}}
```
<span class="caption">示例 19-30: crate 用户所写的能够使用过程式宏的代码</span>
<span class="caption">示例 20-37: 我们 crate 的用户所写的能够使用过程式宏的代码</span>
运行该代码将会打印 `Hello, Macro! My name is Pancakes!` 第一步是像下面这样新建一个库 crate
@ -124,54 +123,60 @@ $ cargo new hello_macro --lib
<span class="filename">文件名src/lib.rs</span>
```rust,noplayground
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-20-impl-hellomacro-for-pancakes/hello_macro/src/lib.rs}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-38/hello_macro/src/lib.rs}}
```
现在有了一个包含函数的 trait。此时crate 用户可以实现该 trait 以达到其期望的功能,像这样:
<span class="caption">示例 20-38: 一个我们会用于 `derive` 宏的简单 trait</span>
现在有了一个 trait 及其相关函数。此时crate 用户可以像示例 20-39 那样实现该 trait 来达到期望的功能,像这样:
<span class="filename">文件名src/main.rs</span>
```rust,ignore
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-20-impl-hellomacro-for-pancakes/pancakes/src/main.rs}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-39/pancakes/src/main.rs}}
```
然而,他们需要为每一个他们想使用 `hello_macro` 的类型编写实现的代码块。我们希望为其节约这些工作。
<span class="caption">示例 20-39: 如果用户手动编写了一个 `HelloMacro` trait 实现看起来如何</span>
另外,我们也无法为 `hello_macro` 函数提供一个能够打印实现了该 trait 的类型的名字的默认实现Rust 没有反射的能力,因此其无法在运行时获取类型名。我们需要一个在编译时生成代码的宏
然而,他们需要为每一个想要与 `hello_macro` 一同使用的类型编写实现的代码块。我们希望免去他们的这份工作
下一步是定义过程式宏。在编写本部分时,过程式宏必须在其自己的 crate 内。该限制最终可能被取消。构造 crate 和其中宏的惯例如下:对于一个 `foo` 的包来说,一个自定义的派生过程宏的包被称为 `foo_derive` 。在 `hello_macro` 项目中新建名为 `hello_macro_derive` 的包。
另外,我们也无法为 `hello_macro` 函数提供一个能够打印实现了该 trait 的类型的名字的默认实现Rust 没有反射能力,因此其无法在运行时获取类型名。我们需要一个在编译时生成代码的宏。
下一步是定义过程宏。在编写本部分时,过程式宏必须在其自己的 crate 内。该限制最终可能被取消。crate 及其宏 crate 的结构惯例如下:对于一个名为 `foo` 的 crate其自定义 derive 过程宏 crate 通常命名为 `foo_derive`。让我们在 `hello_macro` 项目中,新建一个名为 `hello_macro_derive` 的 crate。
```console
$ cargo new hello_macro_derive --lib
```
由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait同时也必须改变在 `hello_macro_derive`实现的过程宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重导出过程式宏的代码。但现在我们组织项目的方式使编程人员在无需 `derive` 功能时也能够单独使用 `hello_macro`
由于两个 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>
```toml
{{#include ../listings/ch20-advanced-features/listing-20-31/hello_macro/hello_macro_derive/Cargo.toml:6:12}}
{{#include ../listings/ch20-advanced-features/listing-20-40/hello_macro/hello_macro_derive/Cargo.toml:6:12}}
```
为定义一个过程式宏,请将示例 19-31 中的代码放在 `hello_macro_derive` crate 的 *src/lib.rs* 文件里面。注意这段代码在我们添加 `impl_hello_macro` 函数的定义之前是无法编译的。
为定义一个过程式宏,请将示例 20-40 中的代码放在 `hello_macro_derive` crate 的 *src/lib.rs* 文件里面。注意这段代码在我们添加 `impl_hello_macro` 函数的定义之前是无法编译的。
<span class="filename">文件名hello_macro_derive/src/lib.rs</span>
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-31/hello_macro/hello_macro_derive/src/lib.rs}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-40/hello_macro/hello_macro_derive/src/lib.rs}}
```
<span class="caption">示例 19-31: 大多数过程式宏处理 Rust 代码时所需的代码</span>
<span class="caption">示例 20-40: 大多数过程式宏处理 Rust 代码时所需的代码</span>
注意我们将代码分成了`hello_macro_derive` 和 `impl_hello_macro` 两个函数,前者负责解析 `TokenStream`,后者负责转换语法树:这使得编写过程宏更方便。几乎你看到或者创建的每一个过程宏的外部函数(这里是`hello_macro_derive`)中的代码都跟这里是一样的。你放入内部函数(这里是`impl_hello_macro`)中的代码根据你的过程宏的设计目的会有所不同。
注意我们将代码分成了 `hello_macro_derive``impl_hello_macro` 两个函数,前者负责解析 `TokenStream`,后者负责转换语法树:这使得编写过程宏更方便。几乎你看到或者创建的每一个过程宏的外部函数(这里是 `hello_macro_derive`)中的代码都跟这里是一样的。你放入内部函数(这里是 `impl_hello_macro`)中的代码根据你的过程宏的设计目的会有所不同。
现在,我们已经引入了三个新的 crate`proc_macro` 、 [`syn`] 和 [`quote`] 。Rust 自带 `proc_macro` crate因此无需将其加到 *Cargo.toml* 文件的依赖中。`proc_macro` crate 是编译器用来读取和操作我们 Rust 代码的 API。
现在,我们已经引入了三个新的 crate`proc_macro` 、 [`syn`] 和 [`quote`] 。Rust 自带 `proc_macro` crate因此无需将其加到 *Cargo.toml* 文件的依赖中。`proc_macro` crate 是编译器提供用来读取和操作我们 Rust 代码的 API。
`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整的解析器并不是一件简单的工作。
`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` crate 则将 `syn` 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整的解析器并不是一件简单的工作。
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。因为我们已经使用 `proc_macro_derive` 及其指定名称`HelloMacro`对 `hello_macro_derive` 函数进行了注解,指定名称`HelloMacro`就是 trait 名,这是大多数过程宏遵循的习惯
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。我们已使用 `proc_macro_derive` 注解该函数并指定名称 `HelloMacro`,该名称与我们的 trait 名称相匹配;这是大多数过程宏遵循的惯例
该函数首先将来自 `TokenStream``input` 转换为一个我们可以解释和操作的数据结构。这正是 `syn` 派上用场的地方。`syn` 中的 `parse` 函数获取一个 `TokenStream` 并返回一个表示解析出 Rust 代码的 `DeriveInput` 结构体。示例 19-32 展示了从字符串 `struct Pancakes;` 中解析出来的 `DeriveInput` 结构体的相关部分:
该函数首先将来自 `TokenStream``input` 转换为一个我们可以解释和操作的数据结构。这正是 `syn` 派上用场的地方。`syn` 中的 `parse` 函数获取一个 `TokenStream` 并返回一个表示解析出 Rust 代码的 `DeriveInput` 结构体。示例 20-41 展示了从字符串 `struct Pancakes;` 中解析出来的 `DeriveInput` 结构体的相关部分:
```rust,ignore
DeriveInput {
@ -193,7 +198,7 @@ DeriveInput {
}
```
<span class="caption">示例 19-32: 解析示例 19-30 中带有宏属性的代码时得到的 `DeriveInput` 实例</span>
<span class="caption">示例 20-41: 解析示例 20-37 中带有宏属性的代码时得到的 `DeriveInput` 实例</span>
该结构体的字段展示了我们解析的 Rust 代码是一个类单元结构体,其 `ident`identifier表示名字`Pancakes`。该结构体里面有更多字段描述了所有类型的 Rust 代码,查阅 [`syn` 中 `DeriveInput` 的文档][syn-docs] 以获取更多信息。
@ -201,39 +206,39 @@ DeriveInput {
你可能也注意到了,当调用 `syn::parse` 函数失败时,我们用 `unwrap` 来使 `hello_macro_derive` 函数 panic。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!``expect` 来提供关于发生何种错误的更加明确的错误信息。
现在我们有了将注解的 Rust 代码从 `TokenStream` 转换为 `DeriveInput` 实例的代码,让我们来创建在注解类型上实现 `HelloMacro` trait 的代码,如示例 19-33 所示。
现在我们有了将注解的 Rust 代码从 `TokenStream` 转换为 `DeriveInput` 实例的代码,让我们来创建在注解类型上实现 `HelloMacro` trait 的代码,如示例 20-42 所示。
<span class="filename">文件名hello_macro_derive/src/lib.rs</span>
```rust,ignore
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-33/hello_macro/hello_macro_derive/src/lib.rs:here}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-42/hello_macro/hello_macro_derive/src/lib.rs:here}}
```
<span class="caption">示例 19-33: 使用解析过的 Rust 代码实现 `HelloMacro` trait</span>
<span class="caption">示例 20-42: 使用解析过的 Rust 代码实现 `HelloMacro` trait</span>
我们得到一个包含以 `ast.ident` 作为注解类型名字(标识符)的 `Ident` 结构体实例。示例 19-32 中的结构体表明当 `impl_hello_macro` 函数运行于示例 19-30 中的代码上时 `ident` 字段的值是 `"Pancakes"`。因此,示例 19-33`name` 变量会包含一个 `Ident` 结构体的实例,当打印时,会是字符串 `"Pancakes"`,也就是示例 19-30 中结构体的名称。
我们得到一个包含以 `ast.ident` 作为注解类型名字(标识符)的 `Ident` 结构体实例。示例 20-33 中的结构体表明当 `impl_hello_macro` 函数运行于示例 20-31 中的代码上时 `ident` 字段的值是 `"Pancakes"`。因此,示例 20-34`name` 变量会包含一个 `Ident` 结构体的实例,当打印时,会是字符串 `"Pancakes"`,也就是示例 20-37 中结构体的名称。
`quote!` 宏能让我们编写希望返回的 Rust 代码。`quote!` 宏执行的直接结果并不是编译器所期望的所以需要转换为 `TokenStream`。为此需要调用 `into` 方法它会消费这个中间表示intermediate representationIR并返回所需的 `TokenStream` 类型值。
这个宏也提供了一些非常酷的模板机制;我们可以写 `#name` ,然后 `quote!` 会以名为 `name` 的变量值来替换它。你甚至可以做一些类似常用宏那样的重复代码的工作。查阅 [`quote` crate 的文档][quote-docs] 来获取详尽的介绍。
这个宏也提供了一些非常酷的模板机制;我们可以写 `#name` ,然后 `quote!` 会以名为 `name` 的变量值来替换它。你甚至可以做一些类似常用宏那样的重复代码的工作。查阅 [`quote` crate 的文档][quote-docs] 来获取完整的介绍。
我们期望我们的过程式宏能够为通过 `#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` 依赖:
此时,`cargo build` 应该都能成功编译 `hello_macro``hello_macro_derive` 。我们将这些 crate 连接到示例 20-31 的代码中来看看过程宏的行为!在 *projects* 目录下用 `cargo new pancakes` 命令新建一个二进制项目。需要将 `hello_macro``hello_macro_derive` 作为依赖加到 `pancakes` 包的 *Cargo.toml* 文件中去。如果你正将 `hello_macro``hello_macro_derive` 的版本发布到 [crates.io](https://crates.io/) 上,它们将是常规依赖;否则,则可以像下面这样将其指定为 `path` 依赖:
```toml
{{#include ../listings/ch20-advanced-features/no-listing-21-pancakes/pancakes/Cargo.toml:7:9}}
```
把示例 19-30 中的代码放在 *src/main.rs* ,然后执行 `cargo run`:其应该打印 `Hello, Macro! My name is Pancakes!`。其包含了该过程宏中 `HelloMacro` trait 的实现,而无需 `pancakes` crate 实现它;`#[derive(HelloMacro)]` 增加了该 trait 实现。
把示例 20-37 中的代码放在 *src/main.rs* ,然后执行 `cargo run`:其应该打印 `Hello, Macro! My name is Pancakes!`。其包含了该过程宏中 `HelloMacro` trait 的实现,而无需 `pancakes` crate 实现它;`#[derive(HelloMacro)]` 增加了该 trait 实现。
接下来,让我们探索一下其他类型的过程宏与自定义派生宏有何区别。
接下来,让我们探索一下其他类型的过程宏与自定义 `derive` 宏有何区别。
### 类属性宏
类属性宏与自定义派生宏相似,不同的是 `derive` 属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于注解 web 应用程序框架web application framework的函数
类属性宏与自定义 `derive` 宏相似,不同之处在于它们不是为 `derive` 属性生成代码,而是允许你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于注解 web 应用程序框架web application framework的函数
```rust,ignore
#[route(GET, "/")]
@ -259,20 +264,20 @@ pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
let sql = sql!(SELECT * FROM posts WHERE id=1);
```
这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 `macro_rules!` 可以做到的更为复杂的处理。`sql!` 宏应该被定义为如此:
这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 `macro_rules!` 可以做到的更为复杂的处理。`sql!` 宏会被定义为类似如此:
```rust,ignore
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
```
这类似于自定义派生宏的签名:获取括号中的 token并返回希望生成的代码。
这类似于自定义 `derive` 宏的签名:获取括号中的 token并返回希望生成的代码。
## 总结
好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到它们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。
呼!现在你的工具箱中有了一些 Rust 特性,虽然你可能不会经常使用它们,但在非常特定的情况下你会知道它们可用。我们介绍了几个复杂的主题,以便当你在错误信息建议或他人代码中遇到它们时,能够识别这些概念和语法。本章可作为查找解决方案的参考。
接下来,我们将再开始一个项目,将本书所学的所有内容付实践!
接下来,我们将再开始一个项目,将本书所学的所有内容付实践!
[ref]: https://doc.rust-lang.org/reference/macros-by-example.html
[tlborm]: https://veykril.github.io/tlborm/

View File

@ -1,9 +1,9 @@
span.caption {
font-size: .8em;
font-weight: 600;
font-size: 0.8em;
font-weight: 600;
}
span.caption code {
font-size: 0.875em;
font-weight: 400;
font-size: 0.875em;
font-weight: 400;
}

View File

@ -1,8 +1,8 @@
figure.listing {
margin: 0;
margin: 0;
}
.listing figcaption {
font-size: .8em;
font-weight: 600;
font-size: 0.8em;
font-weight: 600;
}

View File

@ -4,10 +4,10 @@
identical while updating the presentation.
*/
.note {
margin: 20px 0;
padding: 0 20px;
color: var(--fg);
background-color: var(--quote-bg);
border-block-start: 0.1em solid var(--quote-border);
border-block-end: 0.1em solid var(--quote-border);
margin: 20px 0;
padding: 0 20px;
color: var(--fg);
background-color: var(--quote-bg);
border-block-start: 0.1em solid var(--quote-border);
border-block-end: 0.1em solid var(--quote-border);
}