<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ HeZean - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ HeZean - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 19:37:50 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/hezean/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Rust 教程 – 通过构建 JSON 解析器学习高级迭代器和模式匹配 ]]>
                </title>
                <description>
                    <![CDATA[ 迭代器和模式匹配是 Rust 中使用最频繁的两个语言特性。如果你写过任何现实世界的应用程序，无论大小，你很可能已经使用过这些特性，无论你是否意识到。 在本教程中，我旨在通过编写一个大量使用这些特性的 JSON 解析器，帮助你理解它们实际上是如何工作的、它们的多种常见用法，以及它们的强大之处。 免责声明 本教程的目标是创建一个在实际开发中广泛使用匹配模式和迭代器的库，而不是编写一个高效或与 JSON 标准完全兼容的 JSON 解析器。 如果你对 JSON 非常熟悉，你会注意到代码中缺少很多东西，最大的缺陷是在遇到无效标记时的错误处理，以及向用户提供反馈或帮助说明 JSON 中的问题。 此外，作为例子，该程序也未处理字符串文本中的转义字符和序列。大多数情况下，代码假定你有一个有效的 JSON。 前提条件 虽然本教程适用于任何经验水平的 Rust 程序员，但对基本迭代器和 Rust 中的模式匹配有一定经验或理解会对你理解本教程有帮助。 本教程也假设你已经熟悉 Rust 的基础概念，例如 traits、structs、enums、for 循环、impl 块等。教程会介绍 itera ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/rust-tutorial-build-a-json-parser/</link>
                <guid isPermaLink="false">6786636651e2a8041e344ecc</guid>
                
                    <category>
                        <![CDATA[ Rust ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Tue, 14 Jan 2025 13:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/01/JSON-Parser-Cover.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/rust-tutorial-build-a-json-parser/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Rust Tutorial – Learn Advanced Iterators &amp; Pattern Matching by Building a JSON Parser</a>
      </p><!--kg-card-begin: markdown--><p>迭代器和模式匹配是 Rust 中使用最频繁的两个语言特性。如果你写过任何现实世界的应用程序，无论大小，你很可能已经使用过这些特性，无论你是否意识到。</p>
<p>在本教程中，我旨在通过编写一个大量使用这些特性的 JSON 解析器，帮助你理解它们实际上是如何工作的、它们的多种常见用法，以及它们的强大之处。</p>
<h2 id="">免责声明</h2>
<p>本教程的目标是创建一个在实际开发中广泛使用匹配模式和迭代器的库，而不是编写一个高效或与 JSON 标准完全兼容的 JSON 解析器。</p>
<p>如果你对 JSON 非常熟悉，你会注意到代码中缺少很多东西，最大的缺陷是在遇到无效标记时的错误处理，以及向用户提供反馈或帮助说明 JSON 中的问题。</p>
<p>此外，作为例子，该程序也未处理字符串文本中的转义字符和序列。大多数情况下，代码假定你有一个有效的 JSON。</p>
<h2 id="">前提条件</h2>
<p>虽然本教程适用于任何经验水平的 Rust 程序员，但对基本迭代器和 Rust 中的模式匹配有一定经验或理解会对你理解本教程有帮助。</p>
<p>本教程也假设你已经熟悉 Rust 的基础概念，例如 <code>traits</code>、<code>structs</code>、<code>enums</code>、<code>for</code> 循环、<code>impl</code> 块等。教程会介绍 <code>iterator</code> 和 <code>match</code>，所以不需要熟悉这些也能从中受益。</p>
<h2 id="">目录</h2>
<ol>
<li><a href="#heading-what-are-iterators-in-rust">Rust 中的迭代器是什么？</a>
<ol>
<li><a href="#heading-how-to-implement-iterators-in-rust">如何在 Rust 中实现迭代器</a></li>
<li><a href="#heading-what-are-peekable-iterators-in-rust">Rust 中的可预览迭代器是什么？</a></li>
</ol>
</li>
<li><a href="#heading-what-is-the-match-statement-in-rust">Rust 中的 match 语句是什么？</a>
<ol>
<li><a href="#heading-how-to-use-iterators-in-match-statements-in-rust">如何在 Rust 的 match 语句中使用迭代器</a></li>
<li><a href="#heading-what-are-match-guards-in-rust">Rust 中的 match 卫语句是什么？</a></li>
<li><a href="#heading-what-is-binding-in-rust">Rust 中的绑定是什么？</a></li>
</ol>
</li>
<li><a href="#heading-how-to-build-a-json-parser-stage-1-reader">如何构建一个 JSON 解析器 – 第一步：Reader</a>
<ol>
<li><a href="#heading-what-is-the-utf-8-byte-encoding">什么是 UTF-8 字节编码？</a></li>
<li><a href="#heading-how-to-read-the-data">如何读取数据</a></li>
<li><a href="#heading-how-to-implement-the-iterator-for-jsonreader">如何为 JsonReader 实现迭代器</a></li>
</ol>
</li>
<li><a href="#heading-how-to-build-a-json-parser-stage-2-prepare-intermediate-data-types">如何构建一个 JSON 解析器 – 第二步：准备中间数据类型</a>
<ol>
<li><a href="#heading-the-value-type">值类型</a></li>
<li><a href="#heading-how-to-add-helpful-conversion-methods">如何添加有用的转换方法</a></li>
</ol>
</li>
<li><a href="#heading-how-to-build-a-json-parser-stage-3-tokenization">如何构建一个 JSON 解析器 – 第三步：分词</a>
<ol>
<li><a href="#heading-how-to-define-expected-valid-tokens">如何定义预期的有效 token</a></li>
<li><a href="#heading-how-to-implement-the-tokenizer-struct">如何实现分词器结构体</a></li>
<li><a href="#heading-how-to-tokenize-an-iterator-of-characters">如何对字符迭代器进行分词</a></li>
<li><a href="#heading-how-to-parse-string-tokens">如何解析字符串 token</a></li>
<li><a href="#heading-how-to-parse-number-tokens">如何解析数字 token</a></li>
<li><a href="#heading-how-to-parse-boolean-tokens">如何解析布尔值 token</a></li>
<li><a href="#heading-how-to-parse-null-literal">如何解析 null 字面量</a></li>
<li><a href="#heading-how-to-parse-delimiters">如何解析分隔符</a></li>
<li><a href="#heading-how-to-parse-a-terminating-character">如何解析终止字符</a></li>
</ol>
</li>
<li><a href="#heading-how-to-build-a-json-parser-stage-4-from-tokens-to-value">如何构建一个 JSON 解析器 – 第四步：将 token 转换为值</a>
<ol>
<li><a href="#heading-how-to-parse-primitives">如何解析基本数据类型</a></li>
<li><a href="#heading-how-to-parse-arrays">如何解析数组</a></li>
<li><a href="#heading-how-to-parse-objects">如何解析对象</a></li>
</ol>
</li>
<li><a href="#heading-how-to-use-the-json-parser">如何使用我们的 JSON 解析器</a></li>
<li><a href="#heading-wrapping-up">总结</a></li>
</ol>
<h2 id="heading-what-are-iterators-in-rust">Rust 中的迭代器是什么？</h2>
<p>迭代器不是新概念，也不是 Rust 独有的。它既是一种模式，同时在大多数编程语言中实现为一种用于处理列表（如数组或向量）或集合（如哈希Map）的对象，允许你遍历这些数据类型和处理其中的个别条目。</p>
<p>在 Rust 中，迭代器是一个非常强大的功能。官方的 Rust 书籍描述它为：</p>
<blockquote>
<p>迭代器模式允许你依次对一个项目序列执行某些任务。迭代器负责迭代每个项目的逻辑以及确定序列何时结束。使用迭代器时，你不必自己重新实现该逻辑。</p>
<p>在 Rust 中，迭代器是_惰性_的，意味着在你调用使用它的方法来消耗它之前，它们不会产生任何效果。</p>
</blockquote>
<p>迭代器是一个对象，它帮助我们方便地依次访问集合（如数组或向量）的元素，而不暴露其实现细节。</p>
<h3 id="heading-how-to-implement-iterators-in-rust">如何在 Rust 中实现迭代器</h3>
<p>迭代器在 Rust 中是通过一系列 trait 实现的，其中最基本的是 <code>Iterator</code> trait。它在标准库中的所有集合上都有实现，也可以为自定义类型实现。</p>
<blockquote>
<p>它要求实现一个简单的方法： <code>next()</code>。该方法返回一个 <code>Option&lt;T&gt;</code>，其中 <code>T</code> 是迭代器所针对的元素类型。当 <code>next()</code> 被调用时（在大多数情况下，这种调用是隐式的，你一般会使用更高级的方法），迭代器为序列中的下一个元素生成 <code>Some(value)</code>，或在迭代完成时生成 <code>None</code>。在大多数情况下，值是 <code>Some</code> 还是 <code>None</code> 同样是隐式的。</p>
</blockquote>
<p>例如，任何实现了 <code>Iterator</code> trait 的对象，都可以直接在 <code>for</code> 循环中使用，循环会隐式地处理 <code>next</code> 方法的调用以及处理值是 <code>Some</code> 还是 <code>None</code>。<code>None</code> 值会触发循环结束。这对于内置类型如数组、切片、向量和哈希map同样适用。</p>
<p>作为示例，让我们为一个简单的自定义类型实现 Iterator trait。你需要在类型中存储迭代器的当前状态。你还可以存储任何需要的附加信息。在这里，我们只需要知道迭代结束时的最大值：</p>
<pre><code class="language-rust">use std::iter::Iterator;

struct CustomType {
    current: usize,
    max: usize,
}

impl CustomType {
    fn new(max: usize) -&gt; Self {
        Self {
            current: 0,
            max,
        }
    }
}

impl Iterator for CustomType {
    type Item = usize;

    fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt; {
        if self.current &gt;= self.max {
            None
        } else {
            self.current += 1;
            Some(self.current)
        }
    }
}

fn main() {
    let custom = CustomType::new(10);

    for item in custom {
        println!("当前项：{item}");
    }
}
</code></pre>
<pre><code># 输出

当前项：1
当前项：2
当前项：3
当前项：4
当前项：5
当前项：6
当前项：7
当前项：8
当前项：9
当前项：10
</code></pre>
<p>Rust 的迭代器是懒加载的，这意味着如果你不使用一个迭代器，它不会做任何计算。也就是说，只有在你真正需要获取下一个值并使用时，它才会去计算下一个值是什么。</p>
<p>这也意味着如果你有一连串的操作，比如 <code>map</code> 和 <code>filter</code>，每个项目会先经过整个管道，然后才会处理下一个项目。这不同于许多其他支持 <code>map</code> 和 <code>filter</code> 作为方法的语言，后者会先对所有操作进行整个 <code>map</code> 处理，然后再执行 <code>filter</code>。</p>
<p>如果仔细考虑一下，相较于其他实现，迭代器使我们能够以更简单的方式编写并行处理管道。</p>
<p>由于 <code>Iterator</code> 只是一个 trait，它允许迭代器通过各种适配器方法进行链式连接和转换成其他迭代器（可以是标准库中的，也可以是自己实现的）。</p>
<h3 id="heading-what-are-peekable-iterators-in-rust">Rust 中的可预览的迭代器是什么？</h3>
<p>很多时候，你需要知道下一个元素是什么，以决定如何操作，而不实际修改迭代器状态以移动到下一个元素。这在解析 token 的过程中特别必要，比如我们在本教程后面将要做的那样。</p>
<p>这就是 <code>Peekable</code> 结构体的用武之地。你可以通过调用 <code>peekable</code> 方法将任意迭代器转换成可预览的迭代器。</p>
<p>让我们看一下之前的例子，看看 Peekable 实际是如何工作的：</p>
<pre><code class="language-rust">use std::iter::Iterator;

struct CustomType {
    current: usize,
    max: usize,
}

impl CustomType {
    fn new(max: usize) -&gt; Self {
        Self {
            current: 0,
            max,
        }
    }
}

impl Iterator for CustomType {
    type Item = usize;

    fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt; {
        if self.current &gt;= self.max {
            None
        } else {
            self.current += 1;
            Some(self.current)
        }
    }
}

fn main() {
    let mut custom = CustomType::new(2).peekable();

    let first = custom.peek();
    println!("{first:?}");

    let second = custom.next();
    println!("{second:?}");

    let third = custom.next();
    println!("{third:?}");

    let fourth = custom.next();
    println!("{fourth:?}");
}
</code></pre>
<pre><code># 输出

Some(1)
Some(1)
Some(2)
None
</code></pre>
<p>我还想向你展示如何在没有 for 循环的情况下手动使用迭代器，这就是为什么你会看到对 <code>next</code> 方法的调用，以及它返回 <code>Option</code> 而不是直接返回值。</p>
<p>另外注意 <code>first</code> 和 <code>second</code> 变量都是 <code>Some(1)</code>。这是因为我们第一次调用 <code>peek</code> 它返回了第一个元素，但没有修改迭代器的状态。</p>
<h2 id="heading-what-is-the-match-statement-in-rust">Rust 中的 match 语句是什么？</h2>
<p><code>match</code> 语句是 Rust 中一种模式匹配的语法，它允许你以简洁的语法有条件地根据复杂条件运行代码。你可以把它看作其他语言中的 <code>switch</code> 语句，但功能更强大。</p>
<p>一个非常简单的 <code>match</code> 语句的例子是：</p>
<pre><code class="language-rust">let value = true;

match value {
    true =&gt; {
        println!("值是 true")
    },
    false =&gt; {
        println!("值是 false")
    }
}
</code></pre>
<p>上面定义的各种条件，也就是 <code>true</code> 和 <code>false</code>，被称为分支。每个分支可以有一个匹配，或多个用竖线 <code>|</code> 运算符分隔匹配，或范围。它们还可以为每个分支指定 <code>guards</code> 和 <code>binding</code>。让我们看看这些分别是什么意思：</p>
<pre><code class="language-rust">// 每个分支多个条件

let value = "some_string";

match value {
    "some_string1" | "some_string2" | "some_string3" =&gt; {
        println!("不好的匹配");
    }
    "some_string" =&gt; {
        println!("好的匹配");
    }
    _ =&gt; {
        println!("未匹配");
    }
}
</code></pre>
<p>注意上面例子中的 <code>_</code> 分支。<code>match</code> 语句要求你覆盖所有可能的情况。在第一个例子中，由于值是一个布尔值，只有两个可能的值，<code>true</code> 和 <code>false</code>。因此在第一个例子中，我们已经覆盖了所有可能的值。</p>
<p>但是在第二个例子中，我们匹配的值是一个字符串（更确切地说是 <code>&amp;str</code>）。字符串可以是任何值。对于这个例子，写一个能覆盖所有可能情况的 <code>match</code> 语句是不可能的。好在 Rust 有一个特殊的匹配符 <code>_</code> 可以匹配任何值。</p>
<p>如果你对 JavaScript 或 C（或许多其他具有传统 <code>switch</code> 语法的语言）比较熟悉，<code>_</code> 相当于 <code>switch</code> 中的 <code>default</code> 情况，但你不必使用 <code>_</code>，你也可以将其绑定到一个变量并以不同方式处理。我们很快会看看要如何做到这一点。</p>
<h3 id="heading-how-to-use-iterators-in-match-statements-in-rust">如何在 Rust 的 match 语句中使用迭代器</h3>
<p>一个 match 语句允许你使用迭代器作为分支。当匹配的值是迭代器中的某个值时，就会产生一个成功的匹配。例如，假设你在匹配一个 <code>char</code> 类型是否是一个数字。你可以编写一个包含所有数字字符的简单字符迭代器，并将其用作分支：</p>
<pre><code class="language-rust">let value: char = '5';

match value {
    '0'..='9' =&gt; {
        println!("字符是一个数字");
    }
    _ =&gt; {
        println!("字符不是数字");
    }
}
</code></pre>
<p>上述示例将打印 "字符是一个数字"。如果你不熟悉 <code>..=</code> 语法，这是一种简写，用于创建一个范围内的迭代器。在上例中，迭代器从 <code>'0'</code> 字符开始，到 <code>'9'</code> 字符结束，包括之间的所有字符。</p>
<p>你也可以使用 <code>1..5</code> 来创建一个范围在 1 到 5 之间但不包括 5 的迭代器，使其包含 <code>1, 2, 3, 4</code>。</p>
<p>此外，你可以使用一个保存了迭代器的变量作为值，这意味着迭代器不需要在内联中创建：</p>
<pre><code class="language-rust">let list = vec!["1, 2", "3, 4"].iter();
let value = "3, 4";

match value {
    list =&gt; {
        println!("匹配");
    }
    _ =&gt; {
        println!("未匹配");
    }
}
</code></pre>
<p>注意，上述示例在 vec 上调用 <code>.iter()</code>，以在 <code>list</code> 变量中存储迭代器而不是向量。匹配分支不能有方法调用，因此必须在 match 语句之外将值转换为迭代器。</p>
<h3 id="heading-what-are-match-guards-in-rust">Rust 中的 match 卫语句是什么？</h3>
<p>match 语句中的卫语句（guard）是使得某个分支被视为成功匹配需要满足的附加条件。例如，如果你想匹配一组数字，还要判断它们是奇数还是偶数，卫语句就非常有用。</p>
<p>这个语法也非常直观，形式是 <code>&lt;pattern&gt; if &lt;condition&gt; =&gt; {}</code>。</p>
<pre><code class="language-rust">let value: u8 = 5;

match value {
    0..=9 if value % 2 == 0 =&gt; {
        println!("值是偶数");
    }
    0..=9 if value % 2 == 1 =&gt; {
        println!("值是奇数");
    }
    _ =&gt; {
        println!("无效的值");
    }
}
</code></pre>
<p>上述代码将打印 "值是奇数"。</p>
<h3 id="heading-what-is-binding-in-rust">Rust 中的绑定是什么？</h3>
<p>绑定允许你在某个分支中将值存储在可以使用的变量中。它基本上是将匹配值中的某些部分赋值给变量。</p>
<h4 id="">模式绑定</h4>
<p>一个非常简单的例子是将捕获所有的模式绑定到一个变量，而不是用 <code>_</code> 忽略其值。</p>
<pre><code class="language-rust">let value: u8 = 5;

match value {
    0..=9 if value % 2 == 0 =&gt; {
        println!("值是偶数");
    }
    0..=9 if value % 2 == 1 =&gt; {
        println!("值是奇数");
    }
    other_value =&gt; {
        println!("无效的值：{other_value}");
    }
}
</code></pre>
<p>请注意在这个例子中，如果 match 没有匹配到前面的任何模式，将会被最后一个模式捕获，其中，使用变量 <code>other_value</code> 绑定了 <code>value</code> 的值。然后我们可以在该分支的逻辑中使用这个变量。这里我们只是将其打印出来。</p>
<p>一些其他的绑定例子有：</p>
<pre><code class="language-rust">let value: Option&lt;i32&gt; = Some(43);

match value {
    Some(matched_value) =&gt; println!("值是 {matched_value}"),
    None =&gt; println!("值为空")
}
</code></pre>
<p>在此示例中，我们在 <code>Some</code> 模式中绑定了值以存储选项的内部值，并在我们的逻辑中使用它。</p>
<pre><code class="language-rust">pub struct Person {
    name: String,
    age: u32,
}

let value: Option&lt;Person&gt; = Some(Person {
    name: "Name".to_string(),
    age: 23,
});

match value {
    Some(Person { name: person_name, age }) =&gt; {
        println!("{person_name} 的年龄是 {age} 岁");
    },
    None =&gt; {
        println!("值为空");
    }
}
</code></pre>
<p>我们在这个例子中看到两种不同类型的绑定。第一种是通过解构为结构体字段赋予不同的名称（<code>name</code> 字段），第二种是使用与字段名称相同的名称（<code>age</code> 字段）。</p>
<h4 id=""><code>@</code> 绑定</h4>
<p>Rust 官方文档描述为：</p>
<blockquote>
<p>运算符 @ 允许我们在测试值以匹配模式的同时创建一个保存该值的变量。</p>
</blockquote>
<p>在我们针对一组值或者针对迭代器进行模式匹配的例子中，我们可以使用这种语法将匹配到的值绑定到一个变量，以便在该分支中使用它：</p>
<pre><code class="language-rust">let value: u8 = 5;

match value {
    digit @ 0..=9 =&gt; {
        println!("匹配到的值是 {digit}");
    }
    _ =&gt; {
        println!("无效的值");
    }
}
</code></pre>
<p>这里我们将迭代器中匹配的值绑定到变量 <code>digit</code>，然后在分支中使用它来读取实际值。</p>
<h2 id="heading-how-to-build-a-json-parser-stage-1-reader">如何构建一个 JSON 解析器 – 第一步：Reader</h2>
<p>在解析传入的 JSON 数据之前，我们需要能够以有助于解析的方式读取它。为了能够对传入的 JSON 进行标记，我们需要对每个字符逐个分析，并根据它们是表示字面值、分隔符还是无效值，决定如何处理它们以及后续字符。</p>
<p>这是迭代器与 Rust 的 match 语法结合使用的一个非常好的案例。</p>
<p>我们的读取器需要保存两个数据。一个缓冲读取器，用于遍历输入；一个字符缓冲器，用于保存当前正在解析的字符。</p>
<p>此时，你可能会问为什么我们需要在读取器中保存字符缓冲器，原因是 JSON 是 UTF-8 编码的。</p>
<h3 id="heading-what-is-the-utf-8-byte-encoding">什么是 UTF-8 字节编码？</h3>
<p>一个 UTF-8 字符可以长为 1 到 4 个字节。我们需要能够解析所有有效字符，因为 JSON 规范支持这些字符。这意味着 JSON 字符可以是 1 个字节、2 个字节、3 个字节或 4 个字节长。</p>
<p>对于每次迭代，我们需要一次读取 4 个字节，确定这 4 个字节包含多少个字符（例如，这 4 个字节可以包含 4 个 1 字节的字符），完成对它们的迭代，然后继续读取下一个 4 个字节并重复该过程。为了存储这段中间信息，我们需要字符缓冲区。</p>
<p>也可能我们在当前的 4 个字节中只有部分字符。例如，如果你考虑 2 个 1 字节字符，后跟 1 个 3 字节字符，如 <code>23€</code>，第一个 4 个字节将包含 2 个有效字符和下一个有效字符的一部分。你也需要能够处理这种情况，这将涉及重置迭代器。</p>
<p>可以以一种不需要分配内存的方式处理这种情况，并且出于性能原因实际上这样做更好。但我将留给你作为读者来思考如何在这种情况下实现它，因为这不是本文的重点。</p>
<p>我希望现在你已经清楚了为什么迭代器是这里最合适的工具。</p>
<h3 id="heading-how-to-read-the-data">如何读取数据</h3>
<p>我们将支持两种不同的读取器。一种是直接从缓冲读取器（通常是从文件创建的）读取，另一种是从字节迭代器读取。</p>
<p>这些将相当直接。要从文件读取，你需要在底层文件数据上创建一个缓冲光标：</p>
<pre><code class="language-rust">let file = File::create("dummy.json").unwrap();
let reader = BufReader::new(file);
</code></pre>
<p>让我们从实现 JSON Reader 结构体及其上的方法开始：</p>
<pre><code class="language-rust">// src/reader.rs

use std::collections::VecDeque;
use std::io::{BufReader, Cursor, Read, Seek};
use std::str::from_utf8;

/// 处理要解析的输入数据读取并提供按字符迭代数据的结构体。
pub struct JsonReader&lt;T&gt;
where
    T: Read + Seek,
{
    /// 输入数据的引用，可以是实现了 [`Read`] 的任何内容
    reader: BufReader&lt;T&gt;,

    /// 一个字符缓冲区，保存供迭代器使用的字符队列。
    ///
    /// 这是必要的，因为 UTF-8 可以是 1-4 个字节长。
    /// 因此，读取器总是一次读取 4 个字节。然后，我们迭代“字符”，无论它们是 1 个字节长还是 4 个字节长。
    ///
    /// 使用 [`VecDeque`] 而不是普通向量，因为字符需要从缓冲区的开始处读取。
    character_buffer: VecDeque&lt;char&gt;,
}

impl&lt;T&gt; JsonReader&lt;T&gt;
where
    T: Read + Seek,
{
    /// 创建一个新的 [`JsonReader`] 来从文件读取
    ///
    /// # 示例
    ///
    /// ```
    /// use std::fs::File;
    /// use std::io::BufReader;
    /// use json_parser::reader::JsonReader;
    ///
    /// let file = File::create("dummy.json").unwrap();
    /// let reader = BufReader::new(file);
    ///
    /// let json_reader = JsonReader::new(reader);
    /// ```
    pub fn new(reader: BufReader&lt;T&gt;) -&gt; Self {
        JsonReader {
            reader,
            character_buffer: VecDeque::with_capacity(4),
        }
    }

    /// 创建一个新的 [`JsonReader`] 从给定的字节流读取
    ///
    /// # 示例
    ///
    /// ```
    /// use std::io::{BufReader, Cursor};
    /// use json_parser::reader::JsonReader;
    ///
    /// let input_json_string = r#"{"key1":"value1","key2":"value2"}"#;
    ///
    /// let json_reader = JsonReader::&lt;Cursor&lt;&amp;'static [u8]&gt;&gt;::from_bytes(input_json_string.as_bytes());
    /// ```
    #[must_use]
    pub fn from_bytes(bytes: &amp;[u8]) -&gt; JsonReader&lt;Cursor&lt;&amp;[u8]&gt;&gt; {
        JsonReader {
            reader: BufReader::new(Cursor::new(bytes)),
            character_buffer: VecDeque::with_capacity(4),
        }
    }
}
</code></pre>
<h3 id="heading-how-to-implement-the-iterator-for-jsonreader">如何为 <code>JsonReader</code> 实现迭代器</h3>
<p>接下来，你需要在这个 <code>JsonReader</code> 上实现 <code>Iterator</code> trait，以便于解析。</p>
<p>首先，如果字符缓冲区不为空，你可以从迭代器中返回缓冲区中的第一个字符：</p>
<pre><code class="language-rust">if !self.character_buffer.is_empty() {
    return self.character_buffer.pop_front();
}
</code></pre>
<p>如果它为空，你需要创建一个新的缓冲区并从读取器中读取到该缓冲区：</p>
<pre><code class="language-rust">let mut utf8_buffer = [0, 0, 0, 0];
let _ = self.reader.read(&amp;mut utf8_buffer);
</code></pre>
<p>在这里，你创建了一个大小为 4 的新数组，并将从读取器中读取 4 个字节到其中。</p>
<p>接下来，你需要将其解析为 UTF-8。Rust 为你提供了一个 <code>from_utf8</code> 函数，它将尝试将给定字节解析为 UTF-8。如果有效，它返回一个包含解析字符的字符串。</p>
<p>如果无效，返回的错误信息中会包含无效字节的数量，你可以用来回溯读取器以仅保留有效字符，并从失败点尝试下一个 4 个字符。</p>
<p>如果这不太容易理解，查看代码会让事情变得清晰：</p>
<pre><code class="language-rust">match from_utf8(&amp;utf8_buffer) {
    Ok(string) =&gt; {
        self.character_buffer = string.chars().collect();
        self.character_buffer.pop_front()
    }
    Err(error) =&gt; {
        // 读取有效字节，并回溯缓冲读取器以便下次迭代时可以重新读取剩余字节。

        let valid_bytes = error.valid_up_to();
        let string = from_utf8(&amp;utf8_buffer[..valid_bytes]).unwrap();

        let remaining_bytes = 4 - valid_bytes;

        let _ = self.reader.seek_relative(-(remaining_bytes as i64));

        // 将有效字符收集到字符缓冲区
        self.character_buffer = string.chars().collect();
    }
}
</code></pre>
<p>以下是 <code>Iterator</code> trait 的完整实现：</p>
<pre><code class="language-rust">// src/reader.rs

impl&lt;T&gt; Iterator for JsonReader&lt;T&gt;
where
    T: Read + Seek,
{
    type Item = char;

    #[allow(clippy::cast_possible_wrap)]
    fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt; {
        if !self.character_buffer.is_empty() {
            return self.character_buffer.pop_front();
        }

        let mut utf8_buffer = [0, 0, 0, 0];
        let _ = self.reader.read(&amp;mut utf8_buffer);

        match from_utf8(&amp;utf8_buffer) {
            Ok(string) =&gt; {
                self.character_buffer = string.chars().collect();
                self.character_buffer.pop_front()
            }
            Err(error) =&gt; {
                // 读取有效字节，并重置缓冲读取器
                // 以便在下一次迭代中可以再次读取剩余字节。

                let valid_bytes = error.valid_up_to();
                let string = from_utf8(&amp;utf8_buffer[..valid_bytes]).unwrap();

                let remaining_bytes = 4 - valid_bytes;

                let _ = self.reader.seek_relative(-(remaining_bytes as i64));

                // 收集有效字符到 character_buffer 中
                self.character_buffer = string.chars().collect();

                // 从 character_buffer 中返回第一个字符
                self.character_buffer.pop_front()
            }
        }
    }
}
</code></pre>
<p>这就是你需要做的读取输入数据以便进行解析的所有操作。现在，是时候进入处理的下一个阶段了。</p>
<h2 id="heading-how-to-build-a-json-parser-stage-2-prepare-intermediate-data-types">如何构建一个 JSON 解析器 – 第二步：准备中间数据类型</h2>
<p>这实际上不算是解析管道中的一个阶段，但它是接下来的步骤的前提条件。我们需要定义可以与 JSON 所支持的所有可能类型相匹配的 Rust 类型。</p>
<p>JSON 支持以下数据类型：</p>
<ul>
<li>字符串</li>
<li>数字</li>
<li>布尔值</li>
<li>数组</li>
<li>对象</li>
<li>空值</li>
</ul>
<p>数字可以进一步分为整数或浮点数。尽管你可以使用 <code>f64</code> 作为所有 JSON 数字的 Rust 类型，但在实际操作中，如果你尝试使用它，代码中将到处都是类型转换，这会使其不可行。</p>
<p>所以在本教程中，我们将确实区分这一点并记录下来。</p>
<h3 id="heading-the-value-type">值类型</h3>
<p>枚举是存储这种状态的理想方式，其中每个变体需要有一些标识符作为元数据（在本例中是 JSON 值的类型），并可附加一些数据。你将把 JSON 中该类型的实际值附加到这些变体的数据中。</p>
<pre><code class="language-rust">// src/value.rs

use std::collections::HashMap;

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Number {
    I64(i64),
    F64(f64),
}

#[derive(Debug, PartialEq, Clone)]
pub enum Value {
    String(String),
    Number(Number),
    Boolean(bool),
    Array(Vec&lt;Value&gt;),
    Object(HashMap&lt;String, Value&gt;),
    Null,
}
</code></pre>
<p>前几个变体非常简单，你定义了变体，且其持有的数据是相应的 Rust 类型。最后一个变体更简单，表示 <code>null</code> 值，不需要存储其他数据。</p>
<p>而 <code>Array</code> 和 <code>Object</code> 变体则稍微有趣一点，因为它们递归地存储枚举本身。这说得通，因为 JSON 中的数组可以有任何 JSON 规范支持的值类型。而 JSON 中的对象总是拥有字符串键以及支持的任意 JSON 值，包括其他对象。</p>
<h3 id="heading-how-to-add-helpful-conversion-methods">如何添加有用的转换方法</h3>
<p>你还将需要一种方法将枚举类型转换为基本类型，并在基础数据不是你所期望的情况下抛出错误。这基本上是样板代码，所以我将在不做进一步解释的情况下将它们组合在一起：</p>
<pre><code class="language-rust">// src/value.rs

impl TryFrom&lt;&amp;Value&gt; for String {
    type Error = ();

    fn try_from(value: &amp;Value) -&gt; Result&lt;Self, ()&gt; {
        match value {
            Value::String(value) =&gt; Ok(value.clone()),
            _ =&gt; Err(()),
        }
    }
}

impl TryFrom&lt;&amp;Value&gt; for i64 {
    type Error = ();

    #[allow(clippy::cast_possible_truncation)]
    fn try_from(value: &amp;Value) -&gt; Result&lt;Self, ()&gt; {
        match value {
            Value::Number(value) =&gt; match value {
                Number::I64(value) =&gt; Ok(*value),
                Number::F64(value) =&gt; Ok(*value as i64),
            },
            _ =&gt; Err(()),
        }
    }
}

impl TryFrom&lt;&amp;Value&gt; for f64 {
    type Error = ();

    fn try_from(value: &amp;Value) -&gt; Result&lt;Self, ()&gt; {
        match value {
            Value::Number(value) =&gt; match value {
                Number::F64(value) =&gt; Ok(*value),
                Number::I64(value) =&gt; Ok(*value as f64),
            },
            _ =&gt; Err(()),
        }
    }
}

impl TryFrom&lt;&amp;Value&gt; for bool {
    type Error = ();

    fn try_from(value: &amp;Value) -&gt; Result&lt;Self, ()&gt; {
        match value {
            Value::Boolean(value) =&gt; Ok(*value),
            _ =&gt; Err(()),
        }
    }
}

impl&lt;'a&gt; TryFrom&lt;&amp;'a Value&gt; for &amp;'a Vec&lt;Value&gt; {
    type Error = ();

    fn try_from(value: &amp;'a Value) -&gt; Result&lt;Self, ()&gt; {
        match value {
            Value::Array(value) =&gt; Ok(value),
            _ =&gt; Err(()),
        }
    }
}

#[allow(clippy::implicit_hasher)]
impl&lt;'a&gt; TryFrom&lt;&amp;'a Value&gt; for &amp;'a HashMap&lt;String, Value&gt; {
    type Error = ();

    fn try_from(value: &amp;'a Value) -&gt; Result&lt;Self, ()&gt; {
        match value {
            Value::Object(value) =&gt; Ok(value),
            _ =&gt; Err(()),
        }
    }
}
</code></pre>
<h2 id="heading-how-to-build-a-json-parser-stage-3-tokenization">如何构建一个 JSON 解析器 – 第三步：分词</h2>
<p>下一步是对输入数据进行分词。</p>
<p>分词是将大块的输入拆分为更小、更易处理，并可独立地进行分析的单元。这也使得你更容易处理这些单元而不是字节流，并且它们有助于将传入数据表示为标准格式，并允许将 token 映射到输出值类型。</p>
<p>解析器将递归处理所有标记，直到没有要处理的内容，一旦完成，给我们解析后的数据。</p>
<h3 id="heading-how-to-define-expected-valid-tokens">如何定义预期的有效 token</h3>
<p>这里与我们上面介绍的值类型会有一些重复，但这是预料之中的，因为任何字面值的标记表示将是其自身。在这种情况下，没有办法将其更小的拆分。</p>
<p>同样地，枚举是这里合适的数据类型，因为我们需要元数据（作为标记类型），并可选择其关联的数据。</p>
<p>表示字面值的标记可以这样定义：</p>
<pre><code class="language-rust">// src/token.rs

use std::io::{Read, Seek};
use std::iter::Peekable;
use crate::reader::JsonReader;

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Number {
    I64(i64),
    F64(f64),
}

#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    String(String),
    Number(Number),
    Boolean(bool),
    Null,
}
</code></pre>
<p>除此之外，我们在 JSON 中还有许多其他标记，它们构成了 JSON 格式的“语法”。这些是：</p>
<ul>
<li>花括号（<code>{</code> 或 <code>}</code>）分别表示对象的开始和结束。</li>
<li>方括号（<code>[</code> 或 <code>]</code>）分别表示数组的开始和结束。</li>
<li>冒号（<code>:</code>）用于分隔对象内的键值对。</li>
<li>逗号（<code>,</code>）用于分隔值。</li>
<li>引号（<code>"</code>）表示字符串字面值的开始/结束。</li>
</ul>
<p>所有这些都不需要与任何数据关联，所以它们将在枚举中作为单元变体。将这些添加进去，完整的枚举将是：</p>
<pre><code class="language-rust">// src/token.rs

use std::io::{Read, Seek};
use std::iter::Peekable;
use crate::reader::JsonReader;
use crate::value::Number;

#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    CurlyOpen,
    CurlyClose,
    Quotes,
    Colon,
    String(String),
    Number(Number),
    ArrayOpen,
    ArrayClose,
    Comma,
    Boolean(bool),
    Null,
}
</code></pre>
<h3 id="heading-how-to-implement-the-tokenizer-struct">如何实现分词器结构体</h3>
<p>你将需要一个 <code>JsonTokenizer</code> 结构体来进行分词，同时负责保持分词过程的状态：</p>
<pre><code class="language-rust">// src/token.rs

pub struct JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    tokens: Vec&lt;Token&gt;,
    iterator: Peekable&lt;JsonReader&lt;T&gt;&gt;,
}

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
where
    T: Read + Seek,
{
    pub fn new(reader: File) -&gt; JsonTokenizer&lt;File&gt; {
        let json_reader = JsonReader::&lt;File&gt;::new(BufReader::new(reader));

        JsonTokenizer {
            iterator: json_reader.peekable(),
            tokens: vec![],
        }
    }

    pub fn from_bytes&lt;'a&gt;(input: &amp;'a [u8]) -&gt; JsonTokenizer&lt;Cursor&lt;&amp;'a [u8]&gt;&gt; {
        let json_reader = JsonReader::&lt;Cursor&lt;&amp;'a [u8]&gt;&gt;::from_bytes(input);

        JsonTokenizer {
            iterator: json_reader.peekable(),
            tokens: Vec::with_capacity(input.len()),
        }
    }
}
</code></pre>
<p>在这种情况下，我们使其对输入来源进行泛化。类型 T 需要实现 <code>Read</code> 和 <code>Seek</code> trait，其原因将在稍后解释。</p>
<p>迭代器还需要是 <code>Peekable</code> 的，这意味着我们应该能够在不改变迭代器本身的状态下读取迭代器中的下一个项。</p>
<h3 id="heading-how-to-tokenize-an-iterator-of-characters">如何对字符迭代器进行分词</h3>
<p>一旦定义了所有预期的 token，你需要获取字符迭代器并将其转换为 token 列表，其中每个条目是上一节中定义的 <code>Token</code> 枚举的一种。</p>
<p>我们将通过编写一个检测传入字符的框架函数开始，如果遇到无效标记则抛出 panic：</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt; where
    T: Read + Seek, {
    pub fn tokenize_json(&amp;mut self) -&gt; Result&lt;&amp;[Token], ()&gt; {
        while let Some(character) = self.iterator.peek() {
            match *character {
                // 解析其他所有标记
                // ...
                character =&gt; {
                    if character.is_ascii_whitespace() {
                        continue;
                    }

                    panic!("Unexpected character: ;{character};")
                }
            }
        }

        Ok(&amp;self.tokens)
    }
}
</code></pre>
<p>这里有两个值得注意的地方，我们从简单的开始。如果你的匹配块没有遇到任何已知字符（你将很快实现这一点），你需要一个“兜底”条件来匹配任何字符。</p>
<p>在这里，我们将忽略任何空白字符并在遇到时继续到下一次迭代。如果该字符不是空白字符，那么这里你需要抛出 panic（或返回错误）。</p>
<p>接下来值得注意的是 <code>self.iterator.peek()</code>。为了便于解析分隔符、字面值等不同类型的标记，迭代器在读出下一个字符时不应该推进。这样才能根据下一个字符有条件地推进迭代器。</p>
<p>你还需要将某些标记集的解析委托给不同的函数，这些函数将有自己的逻辑来推进迭代器。</p>
<p>一个很好的例子是解析 <code>null</code> 字面值。如果匹配遇到一个 <code>n</code> 字符且不在字符串、对象、数字等中，则需要确保接下来的三个字符分别是 <code>u</code>、<code>l</code>、<code>l</code>，以形成字面值 <code>null</code>，然后将迭代器前进四个，以便下一个循环在 <code>null</code> 字符之后而不是中间开始解析。</p>
<h3 id="heading-how-to-parse-string-tokens">如何解析字符串 token</h3>
<p>我们将从解析字符串开始。让我们停顿一下，逐步思考需要发生什么：</p>
<ul>
<li>检查匹配是否遇到 <code>"</code> 字符。如果是，将 <code>Token::Quote</code> 推入输出标记列表。</li>
<li>推进迭代器，因此下一个步骤从 <code>"</code> 字符之后开始。</li>
<li>解析所有字符作为字符串的一部分，直到遇到另一个 <code>"</code> 字符，它表示字符串值的结束。</li>
<li>将迭代器向前推进解析为字符串的字符数，以及额外再推进一次，以跳过表示字符串结尾的 <code>"</code> 字符。</li>
<li>将解析值的 <code>Token::String</code> 推入输出标记列表。</li>
<li>将 <code>Token::Quote</code> 推入输出标记列表。</li>
</ul>
<p>希望这不会太令人困惑。但代码应该可以帮助你更好地理解它：</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    // ...

    pub fn tokenize_json(&amp;mut self) -&gt; Result&lt;&amp;[Token], ()&gt; {
        while let Some(character) = self.iterator.peek() {
            match *character {
                '"' =&gt; {
                    // 将打开的引号推入输出标记列表。
                    self.tokens.push(Token::Quotes);

                    // 跳过引号标记，因为我们已经将其添加到标记列表中。
                    let _ = self.iterator.next();

                    // 将解析字符串值委托给一个单独的函数。
                    // 该函数还应妥善处理迭代器的推进。
                    let string = self.parse_string();

                    // 将解析的字符串推入输出标记列表。
                    self.tokens.push(Token::String(string));

                    // 将关闭的引号推入输出标记列表。
                    self.tokens.push(Token::Quotes);
                }
                // ...
            }
        }

        Ok(&amp;self.tokens)
    }

    fn parse_string(&amp;mut self) -&gt; String {
        // 创建新的向量来保存解析的字符。
        let mut string_characters = Vec::&lt;char&gt;::new();

        // 通过引用获取每一个字符，这样它们不会从迭代器中移动出去，
        // 不这么做将要求你将迭代器移动到这个函数中。
        for character in self.iterator.by_ref() {
            // 如果碰到关闭的 `"`, 则跳出循环，因为字符串已结束。
            if character == '"' {
                break;
            }

            // 继续压入矢量以构建字符串。
            string_characters.push(character);
        }

        // 从字符迭代器创建字符串并返回。
        String::from_iter(string_characters)
    }
}
</code></pre>
<p>如前所述，我们在本教程中不会讨论处理转义字符，因为它们对于学习此主题没有太大帮助，但如果你感兴趣，可以在实现的基础上添加这部分内容作为一个很好的练习。</p>
<p>这就完成了字符串解析，我们可以继续解析一个更有趣的值类型了。</p>
<h3 id="heading-how-to-parse-number-tokens">如何解析数字 token</h3>
<p>JSON 规范中的数字有很多变化。它们可以为正或为负，可以是整数或小数。它们还可以表示为科学计数法（例如负指数 <code>3.7e-5</code> 或正指数 <code>3.7e5</code>）。我们需要解析所有这些变体。</p>
<p>一如既往，我们将从简单的部分开始。如果我们遇到任何可能是数字中的有效字符，则需要委托解析到一个 <code>parse_number</code> 函数。但是，任何有效数字只能以一个数字或负号开头。数字不能以小数字符或科学计数法字符开头，这使得我们更加轻松。</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    // ...

    pub fn tokenize_json(&amp;mut self) -&gt; Result&lt;&amp;[Token], ()&gt; {
        while let Some(character) = self.iterator.peek() {
            match *character {
                // ...

                '-' | '0'..='9' =&gt; {
                    let number = self.parse_number()?;
                    self.tokens.push(Token::Number(number));
                }

                // ...
            }
        }

        Ok(&amp;self.tokens)
    }

    // ...
}
</code></pre>
<p>接下来，我们将实现 <code>parse_number</code> 方法：</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    // ...

    fn parse_number(&amp;mut self) -&gt; Result&lt;Number, ()&gt; {
        // 储存解析的数字字符。
        let mut number_characters = Vec::&lt;char&gt;::new();

        // 存储正在解析的数字是否包含“. ”字符，使其成为小数。
        let mut is_decimal = false;

        // 存储在 'e' 或 'E' 符号之后的字符以表示指数值。
        let mut epsilon_characters = Vec::&lt;char&gt;::new();

        // 存储正在解析的数字是否属于 epsilon 字符集合。
        let mut is_epsilon_characters = false;

        while let Some(character) = self.iterator.peek() {
            match character {
                // 匹配表示数字为负的负号
                '-' =&gt; {
                    if is_epsilon_characters {
                        // 如果正在解析 epsilon 字符，将其放入 epsilon 字符集合。
                        epsilon_characters.push('-');
                    } else {
                        // 否则，将其放入正常字符集合。
                        number_characters.push('-');
                    }

                    // 推进迭代器
                    let _ = self.iterator.next();
                }
                // 匹配正号，可以被视为冗余并忽略，因为正号是默认值。
                '+' =&gt; {
                    // 推进迭代器
                    let _ = self.iterator.next();
                }
                // 匹配 0 到 9 之间的任意数字，并存储在 `digit` 变量中。
                digit @ '0'..='9' =&gt; {
                    if is_epsilon_characters {
                        // 如果正在解析 epsilon 字符，将其放入 epsilon 字符集合。
                        epsilon_characters.push(*digit);
                    } else {
                        // 否则，将其放入正常字符集合。
                        number_characters.push(*digit);
                    }
                    // 推进迭代器
                    let _ = self.iterator.next();
                }
                // 匹配表示小数部分开始的小数点。
                '.' =&gt; {
                    // 将小数点字符放入数字字符集合。
                    number_characters.push('.');

                    // 设置当前数字为小数状态。
                    is_decimal = true;

                    // 推进迭代器
                    let _ = self.iterator.next();
                }
                // 匹配表示数字文本值结束的任意字符。可以是分隔键值对的逗号，
                // 闭合对象字符，闭合数组字符，或分隔键与值的 `:`。
                '}' | ',' | ']' | ':' =&gt; {
                    break;
                }
                // 匹配 epsilon 字符，表示这个数字是科学计数法。
                'e' | 'E' =&gt; {
                    // 若已在解析指数数字，则产生错误，因为这意味着有两个 epsilon 字符是无效的。
                    if is_epsilon_characters {
                        panic!("解析数字时遇到无效字符：{character}。遇到双重 epsilon 字符");
                    }

                    // 设置当前数字为科学计数法状态。
                    is_epsilon_characters = true;

                    // 推进迭代器
                    let _ = self.iterator.next();
                }
                // 若遇到其他字符则产生错误。
                other =&gt; {
                    if !other.is_ascii_whitespace() {
                        panic!("解析数字时遇到无效字符：{character}")
                    } else {
                        self.iterator.next();
                    }
                },
            }
        }

        if is_epsilon_characters {
            // 如果数字是指数型，执行计算以将其转换为 Rust 中的浮点数。

            // 以浮点数解析基数。
            let base: f64 = String::from_iter(number_characters).parse().unwrap();

            // 以浮点数解析指数。
            let exponential: f64 = String::from_iter(epsilon_characters).parse().unwrap();

            // 返回最终计算出的十进制数字。
            Ok(Number::F64(base * 10_f64.powf(exponential)))
        } else if is_decimal {
            // 如果数字是小数，在 Rust 中将其解析为浮点数。
            Ok(Number::F64(
                String::from_iter(number_characters).parse::&lt;f64&gt;().unwrap(),
            ))
        } else {
            // 在 Rust 中将数字解析为整数。
            Ok(Number::I64(
                String::from_iter(number_characters).parse::&lt;i64&gt;().unwrap(),
            ))
        }
    }
}
</code></pre>
<p>建议你仔细阅读代码和注释以理解此函数。您应该不会遇到任何未曾涵盖或假设读者已知的语法。</p>
<h3 id="heading-how-to-parse-boolean-tokens">如何解析布尔值 token</h3>
<p>解析布尔值是到目前为止我们看到的最简单的一个。我们需要做的就是匹配首个字符为 <code>t</code> 或 <code>f</code>，然后检查接下来的几个字符以确保它们组成了字面值 <code>true</code> 或 <code>false</code>。</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    // ...

    pub fn tokenize_json(&amp;mut self) -&gt; Result&lt;&amp;[Token], ()&gt; {
        while let Some(character) = self.iterator.peek() {
            match *character {
                // ...

                // 匹配 `t` 字符，表示布尔字面量的开始。
                't' =&gt; {
                    // 推进迭代器
                    let _ = self.iterator.next();

                    // 断言下一字符是 `r`，同时推进迭代器
                    assert_eq!(Some('r'), self.iterator.next());
                    // 断言下一字符是 `u`，同时推进迭代器
                    assert_eq!(Some('u'), self.iterator.next());
                    // 断言下一字符是 `e`，同时推进迭代器
                    assert_eq!(Some('e'), self.iterator.next());

                    // 将字面值推入标记列表中。
                    self.tokens.push(Token::Boolean(true));
                }
                'f' =&gt; {
                    // 推进迭代器
                    let _ = self.iterator.next();

                    // 断言下一字符是 `a`，同时推进迭代器
                    assert_eq!(Some('a'), self.iterator.next());
                    // 断言下一字符是 `l`，同时推进迭代器
                    assert_eq!(Some('l'), self.iterator.next());
                    // 断言下一字符是 `s`，同时推进迭代器
                    assert_eq!(Some('s'), self.iterator.next());
                    // 断言下一字符是 `e`，同时推进迭代器
                    assert_eq!(Some('e'), self.iterator.next());

                    // 将字面值推入标记列表中。
                    self.tokens.push(Token::Boolean(false));
                }

                // ...
            }
        }

        Ok(&amp;self.tokens)
    }
}
</code></pre>
<h3 id="heading-how-to-parse-null-literal">如何解析 null 字面量</h3>
<p>这与我们在前一步解析布尔值非常相似：</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    // ...

    pub fn tokenize_json(&amp;mut self) -&gt; Result&lt;&amp;[Token], ()&gt; {
        while let Some(character) = self.iterator.peek() {
            match *character {
                // ...

                'n' =&gt; {
                    // 推进迭代器
                    let _ = self.iterator.next();

                    // 断言下一字符是 `u`，同时推进迭代器
                    assert_eq!(Some('u'), self.iterator.next());
                    // 断言下一字符是 `l`，同时推进迭代器
                    assert_eq!(Some('l'), self.iterator.next());
                    // 断言下一字符是 `l`，同时推进迭代器
                    assert_eq!(Some('l'), self.iterator.next());

                    // 将空字面值推入输出标记列表。
                    self.tokens.push(Token::Null);
                }

                // ...
            }
        }

        Ok(&amp;self.tokens)
    }
}
</code></pre>
<h3 id="heading-how-to-parse-delimiters">如何解析分隔符</h3>
<p>解析分隔符非常简单。你需要做的就是匹配它们，然后将相应的标记推入输出标记列表：</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    // ...

    pub fn tokenize_json(&amp;mut self) -&gt; Result&lt;&amp;[Token], ()&gt; {
        while let Some(character) = self.iterator.peek() {
            match *character {
                // ...

                '{' =&gt; {
                    self.tokens.push(Token::CurlyOpen);
                    let _ = self.iterator.next();
                }
                '}' =&gt; {
                    self.tokens.push(Token::CurlyClose);
                    let _ = self.iterator.next();
                }
                '[' =&gt; {
                    self.tokens.push(Token::ArrayOpen);
                    let _ = self.iterator.next();
                }
                ']' =&gt; {
                    self.tokens.push(Token::ArrayClose);
                    let _ = self.iterator.next();
                }
                ',' =&gt; {
                    self.tokens.push(Token::Comma);
                    let _ = self.iterator.next();
                }
                ':' =&gt; {
                    self.tokens.push(Token::Colon);
                    let _ = self.iterator.next();
                }

                // ...
            }
        }

        Ok(&amp;self.tokens)
    }
}
</code></pre>
<h3 id="heading-how-to-parse-a-terminating-character">如何解析终止字符</h3>
<p>输入有时可能包含 <code>\0</code> 作为最后一个字符，以指示输入已结束。在处理文件时，这更常被称为 EOF（文件结尾）。它也被称为“转义序列”或“空字符”。</p>
<p>如果遇到这种情况，我们需要处理它并跳出解析循环：</p>
<pre><code class="language-rust">// src/token.rs

impl&lt;T&gt; JsonTokenizer&lt;T&gt;
    where
        T: Read + Seek,
{
    // ...

    pub fn tokenize_json(&amp;mut self) -&gt; Result&lt;&amp;[Token], ()&gt; {
        while let Some(character) = self.iterator.peek() {
            match *character {
                // ...

                '\0' =&gt; break,
                other =&gt; {
                    if !other.is_ascii_whitespace() {
                        panic!("Unexpected token encountered: {other}")
                    } else {
                        self.iterator.next();
                    }
                },

                // ...
            }
        }

        Ok(&amp;self.tokens)
    }
}
</code></pre>
<h2 id="heading-how-to-build-a-json-parser-stage-4-from-tokens-to-value">如何构建一个 JSON 解析器 – 第四步：将 token 转换为值</h2>
<p>现在你已经拥有了所有的标记，是时候进入该过程的最后阶段，将标记转换为你可以在 Rust 代码中使用的真实值。</p>
<p>首先创建一个单元结构体，它可以用作解析器。在这个阶段，我们不需要在整个过程中保存任何状态：</p>
<pre><code class="language-rust">// src/parser.rs

use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Cursor};
use std::iter::Peekable;
use std::slice::Iter;
use crate::token::{JsonTokenizer, Token};
use crate::value::Value;

/// 作为解析 JSON 的入口点的主解析器。
pub struct JsonParser;
</code></pre>
<p>我们还将使用这个作为解析器的公共接口。所以让我们首先实现这些方法：</p>
<pre><code class="language-rust">// src/parser.rs

impl JsonParser {
    /// 创建一个新的 [`JsonParser`] 用于从字节解析 JSON。
    pub fn parse_from_bytes&lt;'a&gt;(input: &amp;'a [u8]) -&gt; Result&lt;Value, ()&gt; {
        let mut json_tokenizer = JsonTokenizer::&lt;BufReader&lt;Cursor&lt;&amp;[u8]&gt;&gt;&gt;::from_bytes(input);
        let tokens = json_tokenizer.tokenize_json()?;

        Ok(Self::tokens_to_value(tokens))
    }

    /// 创建一个新的 [`JsonParser`] 用于从文件解析 JSON。
    pub fn parse(reader: File) -&gt; Result&lt;Value, ()&gt; {
        let mut json_tokenizer = JsonTokenizer::&lt;BufReader&lt;File&gt;&gt;::new(reader);
        let tokens = json_tokenizer.tokenize_json()?;

        Ok(Self::tokens_to_value(tokens))
    }
}
</code></pre>
<p>话不多说，首先你需要实现这些公共方法调用的 <code>tokens_to_value</code> 方法。</p>
<h3 id="heading-how-to-parse-primitives">如何解析基本数据类型</h3>
<p>该方法将负责将标记迭代器作为输入，并输出你之前定义的 <code>Value</code> 类型。这也很简单，因为对象/数组解析被委托给单独的方法，我们稍后会详细介绍。</p>
<pre><code class="language-rust">// src/parser.rs

impl JsonParser {
    // ...

    fn tokens_to_value(tokens: &amp;[Token]) -&gt; Value {
        // 创建一个对标记进行预览的迭代器
        let mut iterator = tokens.iter().peekable();

        // 初始化最终值为 null。
        let mut value = Value::Null;

        // 当迭代器中有 token 时循环。
        // 注意在这种情况下你不需要手动推进迭代器，
        // 这就是为什么你可以直接调用 `iterator.next()`。
        while let Some(token) = iterator.next() {
            match token {
                Token::CurlyOpen =&gt; {
                    value = Value::Object(Self::process_object(&amp;mut iterator));
                }
                Token::String(string) =&gt; {
                    value = Value::String(string.clone());
                }
                Token::Number(number) =&gt; {
                    value = Value::Number(*number);
                }
                Token::ArrayOpen =&gt; {
                    value = Value::Array(Self::process_array(&amp;mut iterator));
                }
                Token::Boolean(boolean) =&gt; value = Value::Boolean(*boolean),
                Token::Null =&gt; value = Value::Null,
                // 忽略所有分隔符，因为当你遇到它们时不需要显式处理任何操作。
                Token::Comma
                | Token::CurlyClose
                | Token::Quotes
                | Token::Colon
                | Token::ArrayClose =&gt; {}
            }
        }

        value
    }
}
</code></pre>
<h3 id="heading-how-to-parse-arrays">如何解析数组</h3>
<p>解析数组几乎和我们上面看到的解析逻辑一样简单。因为数组只是其他 JSON 值的集合，所以解析它们并不像对象那样涉及很多逻辑。</p>
<pre><code class="language-rust">// src/parser.rs

impl JsonParser {
    fn process_array(iterator: &amp;mut Peekable&lt;Iter&lt;Token&gt;&gt;) -&gt; Vec&lt;Value&gt; {
        // 初始化一个 JSON Value 类型的向量，用于保存当前正在解析的数组值。
        let mut internal_value = Vec::&lt;Value&gt;::new();

        // 迭代所有提供的 token。
        while let Some(token) = iterator.next() {
            match token {
                Token::CurlyOpen =&gt; {
                    internal_value.push(Value::Object(Self::process_object(iterator)));
                }
                Token::String(string) =&gt; internal_value.push(Value::String(string.clone())),
                Token::Number(number) =&gt; internal_value.push(Value::Number(*number)),
                Token::ArrayOpen =&gt; {
                    internal_value.push(Value::Array(Self::process_array(iterator)));
                }
                // 如果数组关闭则跳出循环。由于 process_array 的递归性质，
                // 我们无需显式检查关闭标记是否与打开标记匹配。
                Token::ArrayClose =&gt; {
                    break;
                }
                Token::Boolean(boolean) =&gt; internal_value.push(Value::Boolean(*boolean)),
                Token::Null =&gt; internal_value.push(Value::Null),
                // 忽略分隔符
                Token::Comma | Token::CurlyClose | Token::Quotes | Token::Colon =&gt; {}
            }
        }

        internal_value
    }
}
</code></pre>
<h3 id="heading-how-to-parse-objects">如何解析对象</h3>
<p>解析对象比前面的值类型要复杂一些，因为对象带有它们自己的语法。但这应该没什么能让你感到意外的，因此我鼓励你阅读以下代码和注释以了解其工作原理。</p>
<pre><code class="language-rust">impl JsonParser {
    fn process_object(iterator: &amp;mut Peekable&lt;Iter&lt;Token&gt;&gt;) -&gt; HashMap&lt;String, Value&gt; {
        // 表示正在解析的项是键还是值。第一个元素应始终是键，因此初始化为 true。
        let mut is_key = true;

        // 当前解析值对应的键。
        let mut current_key: Option&lt;&amp;str&gt; = None;

        // 已解析对象的当前状态。
        let mut value = HashMap::&lt;String, Value&gt;::new();

        while let Some(token) = iterator.next() {
            match token {
                // 如果是嵌套对象，则递归解析并存储在哈希映射中与当前键关联。
                Token::CurlyOpen =&gt; {
                    if let Some(current_key) = current_key {
                        value.insert(
                            current_key.to_string(),
                            Value::Object(Self::process_object(iterator)),
                        );
                    }
                }
                // 如果遇到此标记，则中断循环，因为它表示正在解析的对象的结束。
                Token::CurlyClose =&gt; {
                    break;
                }
                Token::Quotes | Token::ArrayClose =&gt; {}
                // 如果标记是冒号，则说明是键值对的分隔符。因此，从这个点开始解析的项目将不再是键。
                Token::Colon =&gt; {
                    is_key = false;
                }
                Token::String(string) =&gt; {
                    if is_key {
                        // 如果当前正在解析的是键，则将值设为当前键。
                        current_key = Some(string);
                    } else if let Some(key) = current_key {
                        // 如果进程已经为当前项目设置了键，则解析字符串为值，并在完成后将 current_key 设为 None
                        // 以准备下一个键值对。
                        value.insert(key.to_string(), Value::String(string.clone()));
                        // 将 current_key 设为 None 以准备下一个键值对。
                        current_key = None;
                    }
                }
                Token::Number(number) =&gt; {
                    if let Some(key) = current_key {
                        value.insert(key.to_string(), Value::Number(*number));
                        // 将 current_key 设为 None 以准备下一个键值对。
                        current_key = None;
                    }
                }
                Token::ArrayOpen =&gt; {
                    if let Some(key) = current_key {
                        value.insert(key.to_string(), Value::Array(Self::process_array(iterator)));
                        // 将 current_key 设为 None 以准备下一个键值对。
                        current_key = None;
                    }
                }
                // 如果标记是逗号，则是 JSON 中多个键值对之间的分隔符。因此，从这个点开始解析的项目将是键。
                Token::Comma =&gt; is_key = true,
                Token::Boolean(boolean) =&gt; {
                    if let Some(key) = current_key {
                        value.insert(key.to_string(), Value::Boolean(*boolean));
                        // 将 current_key 设为 None 以准备下一个键值对。
                        current_key = None;
                    }
                }
                Token::Null =&gt; {
                    if let Some(key) = current_key {
                        value.insert(key.to_string(), Value::Null);
                        // 将 current_key 设为 None 以准备下一个键值对。
                        current_key = None;
                    }
                }
            }
        }

        value
    }
}
</code></pre>
<p>现在你应该已经掌握了所有内容，可以用它来在 Rust 中解析一个有效的 JSON 文件了。</p>
<h2 id="heading-how-to-use-the-json-parser">如何使用我们的 JSON 解析器</h2>
<p>让我们在项目中创建一个新的示例来运行我们的 JSON 解析器：</p>
<pre><code class="language-shell">mkdir examples; touch examples/json.rs
</code></pre>
<p>你还需要在 <code>Cargo.toml</code> 文件中将其注册为一个示例：</p>
<pre><code class="language-toml">[package]
name = "json-parser"
version = "0.1.0"
edition = "2021"

[dependencies]

[[example]]
path = "examples/json.rs"
name = "json"
</code></pre>
<p>现在让我们编写代码来运行这个示例。我们首先将一个示例 JSON 文件复制到项目的根目录中，你可以在<a href="https://raw.githubusercontent.com/anshulsanghi-blog/json-parser/master/test.json">这里</a>找到。</p>
<pre><code class="language-rust">// examples/json.rs

use std::fs::File;
use json_parser::parser::JsonParser;

fn main() {
    let file = File::open("test.json").unwrap();
    let parser = JsonParser::parse(file).unwrap();

    dbg!(parser);
}
</code></pre>
<p>使用以下命令运行此代码，你应该会看到与下面相同的输出：</p>
<pre><code class="language-shell">cargo run --example json --release
</code></pre>
<pre><code>[examples/json.rs:8:5] parser = Object(
    {
        "pairs": Array(
            [
                Object(
                    {
                        "x1": Number(
                            F64(
                                41.844453001935875,
                            ),
                        ),
                        "y0": Number(
                            F64(
                                -33.78221816487377,
                            ),
                        ),
                        "y1": Number(
                            F64(
                                -78.10213222087448,
                            ),
                        ),
                        "x0": Number(
                            F64(
                                95.26235434764715,
                            ),
                        ),
                    },
                ),
                Object(
                    {
                        "x0": Number(
                            F64(
                                115.42029308864215,
                            ),
                        ),
                        "y0": Number(
                            F64(
                                1.2002187300000001e-5,
                            ),
                        ),
                        "x1": Number(
                            F64(
                                83.39640643072113,
                            ),
                        ),
                        "y1": Number(
                            F64(
                                28.643090267505812,
                            ),
                        ),
                    },
                ),
                Object(
                    {
                        "isWorking": Boolean(
                            true,
                        ),
                        "sample": String(
                            "string sample",
                        ),
                        "nullable": Null,
                        "isNotWorking": Boolean(
                            false,
                        ),
                    },
                ),
            ],
        ),
        "utf8": Object(
            {
                "key2": String(
                    "value2",
                ),
                "key1": String(
                    "ࠄࠀࠆࠄࠀࠁࠃ",
                ),
            },
        ),
    },
)
</code></pre>
<h2 id="heading-wrapping-up">总结</h2>
<p>我希望你看到了一些有趣的方法，可以利用今天所学来优化你项目中的现有 Rust 代码，以及今后编写的任何涉及这些内容的代码。</p>
<p>你可以在<a href="https://github.com/anshulsanghi-blog/json-parser">这个仓库</a>中找到我们在本文中查看的所有代码的完整内容。</p>
<p>此外，如果你对此主题有任何问题或意见，请随时<a href="mailto:contact@anshulsanghi.tech">联系我</a>。</p>
<h3 id="">喜欢我的作品吗？</h3>
<p>可以考虑请我喝咖啡来支持我的工作！</p>
<p><a href="https://buymeacoffee.com/anshulsanghi">☕请我喝咖啡</a>。</p>
<p>下次再见，祝编码愉快，并祝你晴空万里！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 单元测试是什么？如何在 Rust 中执行单元测试 ]]>
                </title>
                <description>
                    <![CDATA[ 测试是软件开发中至关重要的一部分。测试代码可以确保开发的软件按预期工作，并使其不易受到攻击者的攻击。 软件测试是一个非常广泛的话题。这就是为什么在软件行业中有专门负责 QA （译者注：Quality Assurance，质量保证）和测试的专业人员。这些专业人员通常被称为 QA 工程师。 虽然 QA 是一个独立的领域，但这并不意味着开发人员完全不进行测试。 开发人员进行的最常见的测试是单元测试 。单元测试是一种测试类型，您可以测试小的代码单元（如函数）——因此被称为单元测试。通常通过将预期行为与实际行为进行比较来实现。 单元测试是开发流程中不可或缺的一部分，有些公司的整个开发文化都围绕所谓的测试驱动开发（或 TDD） [https://www.freecodecamp.org/news/test-driven-development-tutorial-how-to-test-javascript-and-reactjs-app/] 展开。 在 TDD 中，开发人员首先编写测试用例（根据功能需求，通常称为用户故事），然后编写满足这些用例的代码。TDD 在需求非常具体的项目中最为出 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/unit-testing-in-rust/</link>
                <guid isPermaLink="false">67172e99384f20043fccf93e</guid>
                
                    <category>
                        <![CDATA[ Rust ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Tue, 22 Oct 2024 04:53:51 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/10/UNIT-TESTING-IN-RUST.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/unit-testing-in-rust/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What is Unit Testing? How to Perform Unit Tests in Rust</a>
      </p><!--kg-card-begin: markdown--><p>测试是软件开发中至关重要的一部分。测试代码可以确保开发的软件按预期工作，并使其不易受到攻击者的攻击。</p>
<p>软件测试是一个非常广泛的话题。这就是为什么在软件行业中有专门负责 QA （译者注：Quality Assurance，质量保证）和测试的专业人员。这些专业人员通常被称为 QA 工程师。</p>
<p>虽然 QA 是一个独立的领域，但这并不意味着开发人员完全不进行测试。</p>
<p>开发人员进行的最常见的测试是<strong>单元测试</strong>。单元测试是一种测试类型，您可以测试小的代码单元（如函数）——因此被称为单元测试。通常通过将预期行为与实际行为进行比较来实现。</p>
<p>单元测试是开发流程中不可或缺的一部分，有些公司的整个开发文化都围绕所谓的<a href="https://www.freecodecamp.org/news/test-driven-development-tutorial-how-to-test-javascript-and-reactjs-app/"><strong>测试驱动开发</strong>（或 TDD）</a>展开。</p>
<p>在 TDD 中，开发人员首先编写测试用例（根据功能需求，通常称为<strong>用户故事</strong>），然后编写满足这些用例的代码。TDD 在需求非常具体的项目中最为出色。</p>
<p>您可以在不同的编程语言中以不同的方式实现单元测试。但单元测试的核心只是对代码的预期行为和实际行为进行对比。</p>
<p>因此，无论在特定语言中如何实现，当您使用任何其他语言时，同样的原则通常适用。</p>
<p>在本教程中，您将学习 Rust 编程语言中的单元测试。尽管本教程并不要求您掌握 Rust 的高级知识，您应该至少了解 <a href="https://www.freecodecamp.org/news/rust-in-replit/">Rust 的编程基础</a>。</p>
<p>本文将涉及：</p>
<ul>
<li>Rust 中单元测试的工作原理</li>
<li>如何在 Rust 中编写单元测试</li>
<li>如何测试一个函数</li>
<li>为什么失败的测试很有用</li>
<li>如何处理预期的错误行为，以便测试不会失败</li>
</ul>
<p>综上所述，让我们继续学习 Rust 的单元测试！</p>
<h1 id="rust">Rust 中单元测试的工作原理</h1>
<p>Rust 以代码安全性为核心构建。Rust 严格的类型注释规则有助于在开发阶段早期消除大量错误。但它仍然不是万无一失的。</p>
<p>像任何其他语言一样，业务逻辑由您负责，您必须帮助 Rust 理解代码中什么是可接受的，什么不是。</p>
<p>是的，这就是我们进行测试的原因。</p>
<p>您不需要安装测试套件即可开始在 Rust 中进行测试，因为它对测试有内置的支持。</p>
<p>首先，在本地机器上创建一个新的 cargo 项目（注意 <code>--lib</code> 标志），并在您选择的文本编辑器或 IDE 中打开它。在本教程中，我将使用 VS Code。</p>
<pre><code class="language-shell">cargo new --lib rust_unit_testing
code rust_unit_testing
</code></pre>
<p>然后，打开 <code>src/lib.rs</code> 文件。这是我们在本教程中将花费最多时间的地方。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/07/1-2.JPG" alt="Rust 库项目的 src/lib.rs 文件" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Rust 库项目的 src/lib.rs 文件</figcaption>
</figure>
<p>在新创建的 Rust 库项目中，您会注意到 <code>lib.rs</code> 文件默认情况下已经被预填了一个示例测试代码。</p>
<p>其主要目的是为您提供编写测试的模板。我们将剖析此简单测试的每个部分并理解 Rust 中的基本测试概念。</p>
<p>首先，让我们了解这几行测试代码在做什么。在此示例中，您将看到在 <code>lib.rs</code> 中定义的测试模块，其中一个测试检查 2 + 2 是否等于 4。</p>
<p>如果您还不了解 Rust 中的模块和属性的概念，那没关系，您可以先忽略它们。</p>
<p>但只是为了给您一个概念，Rust 中的测试是写在 <code>tests</code> 模块（<code>mod tests</code> 部分表示这是测试模块）中，cargo 仅会在测试期间运行任何写在此模块中的内容（这就是 <code>#[cfg(test)]</code> 属性暗示的内容）。</p>
<p>Rust 中的测试本质上只是一个被标记为测试的函数。从上面的示例中，您会注意到 <code>it_works</code> 函数上方的 <code>#[test]</code> 属性。这只是告诉 cargo 该函数是一个测试，应在测试期间调用。</p>
<p>在 <code>it_works</code> 测试函数中，它检查从 2 + 2 得到的 <code>result</code> 值是否等于 4。它使用 <code>assert_eq!</code> 宏执行检查。<code>assert_eq!</code> 宏比较传递给它的左右值的相等性（<code>==</code>）。</p>
<p>在大多数编程语言中，有一个规则是传递给断言的左值应该是预期值，而实际值应该在右边。在 Rust 中，没有严格的规则，您可以将预期和实际结果传递给任意一侧。</p>
<p>现在，尝试使用以下命令运行您的测试：</p>
<pre><code class="language-shell">cargo test
</code></pre>
<p>以下是上面示例的结果：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/07/2-1.JPG" alt="cargo test - 结果" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>cargo test - 结果</figcaption>
</figure>
<p><code>cargo run</code> 命令会让 cargo 执行测试用例，并将测试报告输出到终端。你可以在报告中看到 cargo 运行的测试。</p>
<p>报告的第一行显示 <code>running 1 test</code>，因为我们只有一个测试函数 <code>tests::it_works</code>。在被测试的函数旁边，你会看到 <code>ok</code> 消息，表示测试通过。</p>
<p>你还可以在下面看到结果的摘要：</p>
<ul>
<li>1 个通过</li>
<li>0 个失败</li>
<li>0 个忽略</li>
<li>0 个测量</li>
<li>0 个过滤掉</li>
<li>结果状态为 <code>test result: ok</code></li>
</ul>
<p>这里的 <code>1 passed</code> 计数器表示通过测试的一个测试函数（<code>tests::it_works</code>），而 <code>failed</code> 计数器显示我们有多少测试失败。其他计数器的含义以此类推。</p>
<p>你还会看到 <strong>文档测试</strong> 的结果。由于这里没有任何文档测试，你会看到 <code>running 0 tests</code>。你可以暂时忽略这一点，只关注单元测试。但如果你想了解更多，你可以参考 <a href="https://doc.rust-lang.org/rust-by-example/testing/doc_testing.html">Rust 的官方文档</a>（译者注：亦可参考其<a href="https://rustwiki.org/zh-CN/rust-by-example/testing/doc_testing.html">中文译文</a>）。</p>
<h2 id="rust">如何在 Rust 中编写测试</h2>
<p>编写测试时，你通常需要经过以下三个步骤：</p>
<ol>
<li>模拟测试用例所需的数据或状态。这意味着提供代码所需的模拟或示例数据（如有必要），和/或设置测试用例运行所需的状态或环境。</li>
<li>运行需要测试的代码（传递必要的模拟数据）。例如，调用你想测试的函数。</li>
<li>检查你正在测试的代码的实际行为是否与预期行为相匹配。例如，向一个函数传递参数 <code>x</code>，断言返回值是否与期望的返回值一致。或者检查某段代码给定某个参数时是否引发 <code>panic!</code>，这可能就是预期行为。</li>
</ol>
<p>在 Rust 中，单元测试与被测试代码放在同一个文件中。测试函数通常会放在名为 <code>tests</code> 的模块中（这是约定俗成的命名方式）。</p>
<h3 id="rust">如何在 Rust 中测试函数</h3>
<p>我们现在开始在 Rust 中测试函数。</p>
<p>首先，我们需要一个简单的函数进行测试。但是现在，让我们先移除 <code>it_works</code> 测试函数，因为我们不再需要它。然后，在 <code>tests</code> 模块之上撰写此 <code>adder</code> 函数：</p>
<pre><code class="language-rust">// src/lib.rs

pub fn adder(x: i32, y: i32) -&gt; i32 {
    x + y
}

#[cfg(test)]
mod tests {
// ...
</code></pre>
<p>上面的 <code>adder</code> 函数是一个简单的公共函数，它只是将两个数字相加并返回和。为了测试它是否如预期工作，我们来为这个函数编写一个单元测试。</p>
<p>我们之前讨论的编写单元测试的三个步骤中，前两个步骤是：</p>
<ul>
<li>设置要测试代码所需的数据</li>
<li>运行代码。</li>
</ul>
<p>因此，回到 <code>tests</code> 模块中，首先需要将 <code>adder</code> 函数引入其作用域中（使用 <code>use</code> 关键字）。然后，撰写一个使用 <code>#[test]</code> 属性标注的名为 <code>it_adds</code> 的函数。</p>
<pre><code class="language-rust">// src/lib.rs

pub fn adder (x: i32, y: i32) -&gt; i32 {
    x + y
}

#[cfg(test)]
mod tests {
    // 将父作用域中的所有内容引入此作用域
    use super::*;

    #[test]
    fn it_adds() {
    }
}
</code></pre>
<p><code>it_adds</code> 测试函数内部是我们要编写测试的地方。因此，在其内部声明一个名为 <code>sum</code> 的变量，然后调用 <code>adder</code> 函数并传递 4 和 5 作为其参数（即我们的模拟数据）。</p>
<pre><code class="language-rust">// src/lib.rs

// --省略--

    #[test]
    fn it_adds() {
        let sum = adder(4, 5);
    }
}
</code></pre>
<p>最后，编写单元测试的第三步是检查代码实际行为与预期行为是否一致。</p>
<p>因此，在这里，我们断言 <code>adder</code> 函数返回的 <code>sum</code> 值是否等于 <code>9</code>（这是我们的预期返回值），通过使用 <code>assert_eq!</code> 宏来实现。</p>
<pre><code class="language-rust">// src/lib.rs

// --省略--

    #[test]
    fn it_adds() {
        let sum = adder(4, 5);
        assert_eq!(sum, 9);
    }
}
</code></pre>
<p>这是我们在 <code>lib.rs</code> 文件中代码和测试的最终版本：</p>
<pre><code class="language-rust">// src/lib.rs

pub fn adder(x: i32, y: i32) -&gt; i32 {
    x + y
}

#[cfg(test)]
mod tests {
    // 将父作用域中的所有内容引入此作用域
    use super::*;

    #[test]
    fn it_adds() {
        let sum = adder(4, 5);
        assert_eq!(sum, 9);
    }
}
</code></pre>
<p>正如你之前了解的，你可以使用以下命令运行此测试：</p>
<pre><code class="language-shell">cargo test
</code></pre>
<p>如果一切正常，我们应该看到 <code>test result: ok</code>，表明我们的测试通过了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/3.JPG" alt="Image" width="600" height="400" loading="lazy"></p>
<p>如果你愿意，你还可以在 <code>tests</code> 模块中为 <code>adder</code> 函数添加更多测试（例如，添加负数的测试）。或者更好的是，创建你自己的函数并为其编写一个或多个测试。</p>
<p>此外，Rust 中还有更多内置的断言宏可以使用，除了 <code>assert_eq!</code> 宏。比如，用于断言不等值(<code>!=</code>)的 <code>assert_ne!</code> 宏，以及只断言你正在测试的代码是否返回 <code>true</code> 值的 <code>assert!</code> 宏。</p>
<p>如果你需要更多的断言宏（例如，支持 <code>&gt;</code>、<code>&lt;</code>、<code>&gt;=</code>、<code>&lt;=</code> 的比较断言），你可以安装外部库，比如这个：<a href="https://crates.io/crates/claim">claim</a>。你可以在此处查看 <a href="https://docs.rs/claim/latest/claim/">claim 的文档</a> 以获取更多信息。</p>
<h2 id="">为什么失败的测试是有用的</h2>
<p>到目前为止，我们的测试总是得到通过的结果。</p>
<p>尽管这看起来很棒，但单元测试的真正威力来源于其能捕捉代码中的错误或 bug，并通过失败的测试来报告它们。因此，这次让我们故意编写一段“错误百出”的代码，看看会发生什么。</p>
<p>回到 <code>lib.rs</code> 文件，通过将 <code>adder</code> 函数中的 <code>+</code> 操作符替换为 <code>-</code> 来修改该函数。</p>
<pre><code class="language-rust">// src/lib.rs

pub fn adder(x: i32, y: i32) -&gt; i32 {
    // 将操作符从 '+' 改为 '-'
    x - y
}

// --省略--
</code></pre>
<p>现在再次使用 <code>cargo test</code> 运行测试。正如预期的那样，您应该会看到如下的测试失败结果：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/07/4.JPG" alt="cargo 报告的测试失败" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>cargo 报告的测试失败</figcaption>
</figure>
<p>首先，请注意测试函数 <code>tests::it_adds</code> 的状态显示了一个非常显眼的红色 <code>FAILED</code>。这就是 cargo 测试失败时的表现。</p>
<p>在此之下，您会看到“failures”报告，其中列出了失败的测试及其失败的原因。</p>
<p>从我们的例子中，<code>tests::it_adds</code> 测试失败，根据报告，传入 <code>assert_eq!</code> 宏的左值和右值不相等 (<code>==</code>)。</p>
<p>这是因为左值是 <code>-1</code> 而右值是 <code>9</code>。请记住，在我们的 <code>assert_eq!</code> 断言中，我们传入的左值是存储了 <code>adder(4, 5)</code> 的返回值的 <code>sum</code> 变量。</p>
<p>由于操作符是错误的，<code>adder</code> 函数执行了 <code>4 - 5</code>，而不是期望的 <code>4 + 5</code>。这就是为什么我们获得了 <code>-1</code> 而不是预期值 <code>9</code>。Cargo 发现了这个问题，因此报告了测试失败。</p>
<p>在失败测试报告的下方是其摘要（可以这么说），仍然在“failures”类别下，只是列出了失败测试函数的名称。</p>
<p>最后，整个测试的汇总：</p>
<ul>
<li>状态是：<code>test result: FAILED</code></li>
<li>0 通过</li>
<li>1 失败</li>
<li>0 忽略</li>
<li>0 测量</li>
<li>0 过滤掉</li>
</ul>
<p>这次，我们的 <code>failed</code> 计数器是 <code>1</code>（指的是我们的失败测试函数），而 <code>passed</code> 计数器是 <code>0</code>。</p>
<h2 id="">如何处理预期错误</h2>
<p>从前一节中，您了解到错误会导致测试失败。</p>
<p>但如果您期望测试的代码会失败呢（比如给它一个无效参数）？如果它出现错误，cargo 会将其标记为测试失败，即使您实际上是期望它会失败。</p>
<p>您可以期望失败的行为吗？</p>
<p>简答是：可以的！</p>
<p>为了演示这一点，让我们回到 <code>lib.rs</code> 文件并修改我们的 <code>adder</code> 函数。这次，让我们为其设置一个规则，只接受个位数整数（正数、零和负数）—— 否则，它应该触发 'panic'。为了提高可读性，我们将 <code>adder</code> 函数重命名为 <code>single_digit_adder</code>。</p>
<pre><code class="language-rust">// src/lib.rs

// 修改之前的 `adder` 函数
// 并将其改为 `single_digit_adder`
pub fn single_digit_adder(x: i8, y: i8) -&gt; i8 {
    fn is_single_digit(x: i8) -&gt; bool {
        x &lt; 10 &amp;&amp; x &gt; -10
    }

    if !(is_single_digit(x)) || !(is_single_digit(y)) {
        panic!("只允许个位数整数！");
    } else {
        x + y
    }
}

#[cfg(test)]
mod tests {
// --省略--
</code></pre>
<p>由于我们期望 <code>single_digit_adder</code> 函数在收到非个位数整数时触发 'panic'，因此需要在测试中专门指定这一行为。</p>
<p>为此，我们需要向某个测试函数添加另一个属性：<code>#[should_panic]</code>。</p>
<p>回到 <code>tests</code> 模块，首先编辑 <code>it_adds</code> 测试函数，将 <code>adder</code> 函数调用重命名为 <code>single_digit_adder</code>。</p>
<p>然后，创建一个名为 <code>it_should_only_accept_single_digits</code> 的新测试函数，并添加 <code>#[test]</code> 和 <code>#[should_panic]</code> 属性。</p>
<p>在这个新的测试函数中，调用 <code>single_digit_adder</code> 函数，并传递一个无效参数（<code>11</code>）作为例子。</p>
<pre><code class="language-rust">// src/lib.rs

pub fn single_digit_adder(x: i8, y: i8) -&gt; i8 {
    // ...
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds() {
        let sum = single_digit_adder(2, 3);
        assert_eq!(sum, 5);
    }

    // 我们的新测试函数，在传入无效参数时期望 `panic!`
    #[test]
    #[should_panic]
    fn it_should_only_accept_single_digits() {
        single_digit_adder(11, 4);
    }
}
</code></pre>
<p>在 <code>it_should_only_accept_single_digits</code> 测试函数中不需要任何断言宏，因为我们只需让 <code>single_digit_adder</code> 触发 'panic'。因此，简单地调用该函数就足够了。</p>
<p>通过给予一个无效的参数（<code>11</code>，这不是一个个位数），我们期望它会触发 'panic'。<code>#[should_panic]</code> 属性将预期 <code>it_should_only_accept_single_digits</code> 测试函数内部会出现 panic。如果没有捕获到任何 panic，该测试将失败。只有当 <code>single_digit_adder</code> 触发 panic 时，它才会通过。</p>
<p>所以为了测试它是否真的有效，先尝试注释掉 <code>#[should_panic]</code> 属性，然后运行 <code>cargo test</code>。您应该能够看到它失败。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/6.JPG" alt="Image" width="600" height="400" loading="lazy"></p>
<p>现在，取消注释 <code>#[should_panic]</code> 属性并重新运行测试。您的测试应该会全部通过：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/07/5.JPG" alt="测试用例预期并实际捕获到失败行为的输出" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>测试用例预期并实际捕获到失败行为的输出</figcaption>
</figure>
<p>请注意，在测试 <code>tests::it_should_only_accept_single_digits</code> 上标有 <code>should panic</code>，并且它通过了测试。这意味着此测试函数如预期般捕获到了一个 panic。</p>
<p>就是这样！你刚刚了解了什么是单元测试，以及如何使用 Rust 编程语言执行单元测试。欢迎使用本文所学知识编写自己的测试，并将其用于未来的项目中。</p>
<h1 id="">结论</h1>
<p>在本文中，您了解了单元测试是什么以及它在软件开发过程中的重要性。您还通过简单的三步流程学习了如何编写单元测试，并在 Rust 编程语言中实际进行测试。</p>
<p>我们讨论了 Rust 中测试模块的结构以及如何构建测试函数，然后编写了一个简单的 Rust 程序及一些配套的测试用例。我们还讨论了测试失败以及如何处理代码单元中预期的失败行为。</p>
<p>测试是软件开发过程中重要的一部分。对代码进行测试有助于确保软件按预期工作。作为开发人员，测试代码以确保您发布的软件质量，以及避免那些愚蠢的 bug 到达终端用户是很重要的！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Rust 过程宏初学者手册 ]]>
                </title>
                <description>
                    <![CDATA[ 在这本手册中，你将了解 Rust 中的过程宏（Procedural macros）及其用途。我们还将通过一些虚构的场景以及实际的例子来学习如何编写过程宏。 本指南假定你已经熟悉 Rust 及其基本概念，如数据类型、迭代器和 traits（特质）。如果你需要学习或复习 Rust 的基础知识，请查看这个互动课程 [https://www.freecodecamp.org/news/rust-in-replit/]。 你不需要具备宏的前置知识，因为这篇文章会从头开始进行讲解。 目录  1.  Rust 中的宏是什么？ 1. Rust 中的宏类型       2. 过程宏的类型              2.  准备工作 1. 有用的依赖项    ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/procedural-macros-in-rust/</link>
                <guid isPermaLink="false">67121dba384f20043fccf91a</guid>
                
                    <category>
                        <![CDATA[ Rust ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 手册 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Fri, 18 Oct 2024 09:04:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/10/Procedural-Macros-in-Rust-Cover--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/procedural-macros-in-rust/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Procedural Macros in Rust – A Handbook for Beginners</a>
      </p><!--kg-card-begin: markdown--><p>在这本手册中，你将了解 Rust 中的过程宏（Procedural macros）及其用途。我们还将通过一些虚构的场景以及实际的例子来学习如何编写过程宏。</p>
<!-- more -->
<p>本指南假定你已经熟悉 Rust 及其基本概念，如数据类型、迭代器和 traits（特质）。如果你需要学习或复习 Rust 的基础知识，请<a href="https://www.freecodecamp.org/news/rust-in-replit/">查看这个互动课程</a>。</p>
<p>你不需要具备宏的前置知识，因为这篇文章会从头开始进行讲解。</p>
<h2 id="">目录</h2>
<ol>
<li><a href="#heading-what-are-macros-in-rust">Rust 中的宏是什么？</a>
<ol>
<li><a href="#heading-types-of-macros-in-rust">Rust 中的宏类型</a></li>
<li><a href="#heading-types-of-procedural-macros">过程宏的类型</a></li>
</ol>
</li>
<li><a href="#heading-prerequisites">准备工作</a>
<ol>
<li><a href="#heading-helpful-dependencies">有用的依赖项</a></li>
</ol>
</li>
<li><a href="#heading-how-to-write-a-simple-derive-macro">如何编写一个简单的派生宏</a>
<ol>
<li><a href="#heading-the-intostringhashmap-derive-macro"><code>IntoStringHashMap</code> 派生宏</a></li>
<li><a href="#heading-how-to-declare-a-derive-macro">如何声明一个派生宏</a></li>
<li><a href="#how-to-parse-macro-input">如何解析宏输入</a></li>
<li><a href="#how-to-ensure-a-struct-target-for-macro">如何确保宏的目标是一个结构体</a></li>
<li><a href="#heading-how-to-build-the-output-code">如何构建输出代码</a></li>
<li><a href="#heading-how-to-use-your-derive-macro">如何使用你的派生宏</a></li>
<li><a href="#heading-how-to-improve-our-implementation">如何改进我们的实现</a></li>
</ol>
</li>
<li><a href="#heading-a-more-elaborate-derive-macro">更复杂的派生宏</a>
<ol>
<li><a href="#heading-the-derivecustommodel-macro"><code>DeriveCustomModel</code> 宏</a></li>
<li><a href="#how-to-separate-implementation-from-declaration">如何将实现与声明分离</a></li>
<li><a href="#heading-how-to-parse-derive-macro-arguments">如何解析派生宏的参数</a></li>
<li><a href="#heading-how-to-implement-derivecustommodel">如何实现 <code>DeriveCustomModel</code></a></li>
<li><a href="#heading-how-to-generate-each-custom-model">如何生成每个自定义模型</a></li>
<li><a href="#how-to-use-your-derivecustommodal-macro">如何使用这个 <code>DeriveCustomModal</code> 宏</a></li>
</ol>
</li>
<li><a href="#heading-a-simple-attribute-macro">一个简单的属性宏</a>
<ol>
<li><a href="#heading-the-logduration-attribute"><code>log_duration</code> 属性</a></li>
<li><a href="#heading-how-to-declare-an-attribute-macro">如何声明一个属性宏</a></li>
<li><a href="#heading-how-to-implement-the-logduration-attribute-macro">如何实现 <code>log_duration</code> 属性宏</a></li>
<li><a href="#how-to-use-your-log-duration-macro">如何使用这个 <code>log_duration</code> 宏</a></li>
</ol>
</li>
<li><a href="#heading-a-more-elaborate-attribute-macro">更复杂的属性宏</a>
<ol>
<li><a href="#heading-the-cachedfn-attribute"><code>cached_fn</code> 属性</a></li>
<li><a href="#heading-how-to-implement-the-cachedfn-attribute-macro">如何实现 <code>cached_fn</code> 属性宏</a></li>
<li><a href="#heading-cachedfn-attribute-arguments"><code>cached_fn</code> 的属性参数</a></li>
<li><a href="#heading-how-to-use-the-cachedfn-macro">如何使用 <code>cached_fn</code> 宏</a></li>
</ol>
</li>
<li><a href="#heading-a-simple-function-like-macro">一个简单的函数式宏</a>
<ol>
<li><a href="#heading-the-constantstring-macro"><code>constant_string</code> 宏</a></li>
<li><a href="#heading-how-to-declare-a-function-like-macro">如何声明一个函数式宏</a></li>
<li><a href="#heading-how-to-implement-the-constantstring-macro">如何实现 <code>constant_string</code> 宏</a></li>
<li><a href="#heading-how-to-use-the-constantstring-macro">如何使用 <code>constant_string</code> 宏</a></li>
</ol>
</li>
<li><a href="#heading-a-more-elaborate-function-like-macro">更复杂的函数式宏</a>
<ol>
<li><a href="#heading-the-hashmapify-macro"><code>hash_mapify</code> 宏</a></li>
<li><a href="#heading-how-to-implement-the-hashmapify-macro">如何实现 <code>hash_mapify</code> 宏</a></li>
<li><a href="#how-to-parse-hash-mapifys-input">如何解析 <code>hash_mapify</code> 的输入</a></li>
<li><a href="#how-to-generate-output-code">如何生成输出代码</a></li>
<li><a href="#heading-how-to-convert-custom-data-types-to-output-tokens">如何将自定义数据类型转换为输出 token</a></li>
<li><a href="#heading-how-to-use-the-hashmapify-macro">如何使用 <code>hash_mapify</code> 宏</a></li>
</ol>
</li>
<li><a href="#heading-beyond-writing-macros">编写宏 —— 更进一步</a>
<ol>
<li><a href="#heading-helpful-cratestools">有用的库/工具</a></li>
</ol>
</li>
<li><a href="#heading-downsides-of-macros">宏的缺点</a><br>
1.  <a href="#heading-debugging-or-lack-thereof">调试（或者说缺乏调试）</a><br>
2.  <a href="#heading-compile-time-costs">编译时成本</a><br>
3.  <a href="#heading-lack-of-auto-complete-and-code-checks">缺乏自动补全和代码检查</a><br>
4.  <a href="#heading-where-do-we-draw-the-line">我们应该止步于何处？</a></li>
<li><a href="#heading-wrapping-up">总结</a><br>
1.  <a href="#heading-enjoying-my-work">喜欢我的作品吗？</a></li>
</ol>
<h2 id="heading-what-are-macros-in-rust">Rust 中的宏是什么？</h2>
<p>宏（Macro）是 Rust 编程语言的重要组成部分。一旦你开始学习这门语言，你很快就会遇到它们。</p>
<p>在最简单的形式下，Rust 中的宏允许你在编译时执行一些代码。实际上，Rust 几乎允许你随心所欲地编写和使用宏。此功能最常见的用例是编写代码来生成其他代码。</p>
<p>宏是一种扩展编译器功能的方法，使之可以支持标准之外的功能。无论是基于现有代码生成代码，还是以某种形式转换现有代码，宏都是你的首选工具。</p>
<p>官方的 Rust 书这样描述它：</p>
<blockquote>
<p><em>宏</em> 这个术语指的是 Rust 中的一系列功能。</p>
<p>从根本上说，宏是一种编写代码来编写其他代码的方法，这被称为 <em>元编程（Metaprogramming）</em>。</p>
<p>元编程对于减少必须编写和维护的代码量非常有用，这也是函数的作用之一。然而，宏具有一些函数所没有的额外能力。</p>
</blockquote>
<p>使用宏，你还可以动态添加一些编译时需要添加的内容，这在函数中是不可能的，因为函数是在运行时调用的。例如，在类型上实现 <em>traits</em>，这要求在编译阶段完成。</p>
<p>宏的另一个优势是它们非常灵活，因为它们可以接收动态数量的参数或输入，而函数则不行。</p>
<p>宏确实有其自己特定的语法，无论是编写还是使用它们，我们将在接下来的章节中详细探讨这一点。</p>
<p>一些宏使用的示例非常有助于让你体会到它们的强大之处：</p>
<ul>
<li><strong>SQLx</strong> 项目使用宏在编译时，通过实际在运行的数据库实例中执行所有 SQL 查询和语句，来验证它们（只要你使用提供的宏创建它们）。是的，在编译时。</li>
<li><strong>typed_html</strong> 使用宏实现了一个完整的 HTML 解析器，并在编译时进行验证，同时使用了熟悉的 JSX 语法。</li>
</ul>
<h2 id="heading-types-of-macros-in-rust">Rust 中的宏类型</h2>
<p>在 Rust 中，有两种不同类型的宏：声明性宏（Declarative macros）和过程宏（Procedural macros）。</p>
<h3 id="">声明性宏</h3>
<p>声明性宏基于语法解析工作。虽然官方文档将它们定义为允许你去编写语法扩展，但我认为把它们看作是编译器中 <code>match</code> 关键字的高级版本更为直观。</p>
<p>你可以定义一个或多个匹配模式，它们的主体应返回你希望宏生成的 Rust 代码。</p>
<p>我们在本文中不会讨论它们，但如果你想了解更多，<a href="https://rustwiki.org/zh-CN/reference/macros-by-example.html">这里</a>是一个不错的起点。</p>
<h3 id="">过程宏</h3>
<p>这些宏最基本的使用场景是在编译时执行你希望的任何 Rust 代码。唯一的要求是它们应将 Rust 代码作为输入，并返回 Rust 代码作为输出。</p>
<p>编写这些宏不涉及特殊的语法解析（除非你想这样做），所以对我个人来说，它们更容易理解和编写。</p>
<p>过程宏进一步分为三类：派生宏（Derive macro）、属性宏（Attribute macro）和函数式宏（Function-like macro）。</p>
<h3 id="heading-types-of-procedural-macros">过程宏的类型</h3>
<h4 id="">派生宏</h4>
<p>总体来说，派生宏应用于 Rust 中的数据类型。它们是一种扩展类型声明的方法，允许自动为其“派生”功能。</p>
<p>你可以使用它们从一个类型生成“派生”类型，或者作为一种自动为目标数据类型实现方法的方式。下面的示例能帮助你能更好地理解它。</p>
<p>出于调试的目的，打印非原始数据类型，如结构体、枚举甚至错误（它们其实是结构体，但我们现在假设它们不是），是任何语言都非常常见的功能，不仅仅是 Rust。在 Rust 中，只有原始类型具有在“调试”上下文中打印的能力。</p>
<p>如果你考虑到 Rust 中的一切都是 trait（即使是基本操作，如加法和等式），这就有意义了。你希望能够在调试时打印自定义数据类型，但 Rust 无法说“请将这个 trait 应用于现有代码中的每一个数据类型上”。</p>
<p>这就是 <code>Debug</code> 派生宏的用武之地。有一种标准的方法来调试打印 Rust 内部类型的数据结构。<code>Debug</code> 宏允许你自动为自定义类型实现 <code>Debug</code> 这个 trait，同时遵循与内部数据类型实现相同的规则和样式指南。</p>
<pre><code class="language-rust">// 派生宏示例

/// 为数据类型派生方法的示例
#[derive(Debug)]
pub struct User {
    username: String,
    first_name: String,
    last_name: String,
}
</code></pre>
<p><code>Debug</code> 派生宏会生成如下代码（出于展示目的，不完全准确）：</p>
<pre><code class="language-rust">impl core::fmt::Debug for User {
    fn fmt(&amp;self, f: &amp;mut core::fmt::Formatter) -&gt; core::fmt::Result {
        f.debug_struct(
            "User"
        )
        .field("username", &amp;self.username)
        .field("first_name", &amp;self.first_name)
        .field("last_name", &amp;self.last_name)
        .finish()
    }
}
</code></pre>
<p>正如你可能能看出来，没有人愿意一遍遍地为他们所有的自定义结构体和枚举编写这段代码。这个简单的宏让你感受到了 Rust 中宏的强大，以及为什么它们是语言本身的重要组成部分。</p>
<p>在实际编译过程中，上面的代码会产生以下输出：</p>
<pre><code class="language-rust">pub struct User {
    username: String,
    first_name: String,
    last_name: String,
}

impl core::fmt::Debug for User {
    fn fmt(&amp;self, f: &amp;mut core::fmt::Formatter) -&gt; ::core::fmt::Result {
        f.debug_struct(
            "User"
        )
        .field("username", &amp;self.username)
        .field("first_name", &amp;self.first_name)
        .field("last_name", &amp;self.last_name)
        .finish()
    }
}
</code></pre>
<p>请注意，原始类型声明在输出代码中保留。这是派生宏与其他宏之间的主要区别之一。派生宏保留输入类型而不做修改。它们只向输出中添加附加代码。另一方面，所有其他宏的行为则不相同。它们仅在宏自身的输出中包含目标时才保留目标。</p>
<h4 id="">属性宏</h4>
<p>属性宏除了数据类型外，通常还应用于代码块，如函数、impl 块、内联块等。它们通常用于以某种方式转换目标代码，或使用附加信息注解它。</p>
<p>这些宏最常见的用例是修改函数以添加额外的功能或逻辑。例如，你可以轻松编写一个属性宏：</p>
<ul>
<li>记录所有输入和输出参数</li>
<li>记录函数的总运行时间</li>
<li>统计函数调用次数</li>
<li>向任何结构体添加预定义的附加字段</li>
</ul>
<p>等等。</p>
<p>以上我提到的所有这些内容，以及更多内容，结合起来形成了 Rust 中由 <code>tracing</code> 库提供的非常流行且有用的 <code>instrumentation</code> 宏。当然，我在这里进行了大幅简化，但作为示例已经足够。</p>
<p>如果你习惯使用 Clippy（译者注：Clippy 是一个内置许多规则的代码静态检查工具，可参考<a href="https://rustycab.github.io/LearnRustEasy/chapter_4/chapter_4_2.html">该教程</a>），它可能已经多次提醒你在函数或方法上添加 <code>#[must_use]</code> 属性了。</p>
<p>这是使用宏注解函数附加附加信息的一个示例。它告诉编译器如果这个函数调用的返回值没有被使用，就会警告用户。<code>Result</code> 类型默认已经被注解了 <code>#[must_use]</code>，这就是为什么当你不使用 <code>Result</code> 类型的返回值时会看到警告 <code>Unused Result&lt;...&gt; that must be used</code>。</p>
<p>属性宏也是 Rust 中<a href="https://rustwiki.org/zh-CN/reference/conditional-compilation.html">条件编译</a>的驱动力。</p>
<h4 id="">函数式宏</h4>
<p>函数式宏是伪装成函数的宏。这些是限制最少的过程宏，因为只要它们输出的代码在使用上下文中是有效的，它们几乎可以在任何地方使用。</p>
<p>这些宏与另外两种不同，并不是作用到某些东西上，而是像调用函数一样被调用。对于参数，你可以传入任何你想传的东西，只要你的宏能解析它。这包括没有参数、有效的 Rust 代码或者只有你的宏能理解的乱七八糟的内容。</p>
<p>某种意义上，它们是声明式宏的过程版本。如果你需要执行 Rust 代码并且能够解析自定义语法，函数式宏是你的首选工具。如果你在其他宏不能使用的地方需要类似宏的功能，它们也非常有用。</p>
<p>在对宏的基本信息进行了这么长时间的描述之后，终于可以深入实际编写过程宏了。</p>
<h2 id="heading-prerequisites">准备工作</h2>
<p>编写自己的过程宏有一定的规则，你需要遵循这些规则。这些规则适用于所有三种类型的过程宏。它们是：</p>
<ul>
<li>过程宏只能添加到在 <code>Cargo.toml</code> 中标记为 <code>proc-macro</code> 的项目中</li>
<li>标记为这样的项目不能导出除了过程宏之外的任何东西。</li>
<li>宏本身必须在 <code>lib.rs</code> 文件中声明。</li>
</ul>
<p>让我们使用以下命令开始创建我们的项目：</p>
<pre><code class="language-shell">cargo new --bin my-app
cd my-app
cargo new --lib my-app-macros;
</code></pre>
<p>这将创建一个根项目，以及一个子项目来存放我们的宏。你需要在这两个项目的 <code>Cargo.toml</code> 文件中进行一些更改。</p>
<p>首先，<code>my-app-macros</code> 的 <code>Cargo.toml</code> 文件应该包含以下内容（注意，你需要声明一个包含 <code>proc-macro</code> 属性的 <code>lib</code> 部分）：</p>
<pre><code class="language-toml"># my-app/my-app-macros/Cargo.toml

[package]
name = "my-app-macros"
version = "0.1.0"
edition = "2021"

[lib]
name = "my_app_macros"
path = "src/lib.rs"
proc-macro = true

[dependencies]
</code></pre>
<p>接下来，<code>my-app</code> 的 <code>Cargo.toml</code> 文件应该包含以下内容：</p>
<pre><code class="language-toml"># my-app/Cargo.toml

workspace = { members = ["my-app-macros"] }

[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
resolver = "2"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
my-app-macros = { path = "./my-app-macros" }
</code></pre>
<p>你需要将依赖解析器版本设置为“2”，并将你的宏项目添加为 <code>my-app</code> 项目的依赖。</p>
<h3 id="heading-helpful-dependencies">有用的依赖项</h3>
<p>从编译器的角度来看，宏是这样工作的：</p>
<ul>
<li>它们将一个 token（词元）流作为输入（也可选地接收一系列 token 作为宏本身的参数）。</li>
<li>它们返回一个 token 流作为输出。</li>
</ul>
<p>这就是编译器所知道的全部！正如你将会看到的，这对编译器来说已经足够了。</p>
<p>不过，这确实带来了一个问题。你需要能够以一种正确理解这些“token 流”的方式进行解析，无论它们是 Rust 代码还是自定义语法，能够修改它们，并输出它们。手动完成此任务并不容易，而且它超出了本教程的讨论范围。</p>
<p>然而，我们可以依赖许多开发人员撰写的优秀开源作品来简化这个问题。你需要添加一些依赖项来帮助解决这个问题：</p>
<ul>
<li><code>syn</code>  ——  Rust 的语法解析器。这有助于你将输入的 token 流解析为 Rust AST。AST 是一个你在尝试编写自己的解释器或编译器时经常遇到的概念，但对于宏的工作，基本的理解是必不可少的。毕竟，宏在某种意义上只是你为编译器编写的扩展。如果你对了解更多关于AST的信息感兴趣，可以<a href="https://dev.to/balapriya/abstract-syntax-tree-ast-explained-in-plain-english-1h38">查看这个非常有帮助的介绍</a>。</li>
<li><code>quote</code>  ——  简单来说，quote 是一个帮助我们执行 <code>syn</code> 反向操作的库。它帮助我们将 Rust 源代码转换为可以从宏输出的 token 流。</li>
<li><code>proc-macro2</code>  —— 标准库中有一个 <code>proc-macro</code>，但它提供的类型不能存在于过程宏之外。<code>proc-macro2</code>是一个标准库的包装器，使所有的内部类型在宏的上下文之外也能使用。这允许 <code>syn</code> 和 <code>quote</code> 不仅用于过程宏，还可以在普通 Rust 代码中使用，如果你有这样的需求的话。而且，如果我们想要对我们的宏或其扩展进行单元测试，这将被广泛使用。</li>
<li><code>darling</code> —— 它有助于解析和处理宏的参数，否则由于需要从语法树中手动解析它，这将是一个繁琐的过程。<code>darling</code> 为我们提供了类似 <code>serde</code> 的能力，可以将输入参数树自动解析为我们的参数结构体。它还帮助我们处理无效参数、必需参数等错误。</li>
</ul>
<p>虽然这些项目由许多开发者贡献，但我想特别感谢 <a href="https://crates.io/users/dtolnay">David Tolnay</a>。他是 Rust 社区中的传奇人物，创建了这些项目中的大多数，以及许多其他 Rust 的开源库。</p>
<p>让我们快速将这些依赖项添加到我们的项目中并开始编写宏：</p>
<pre><code class="language-shell">// my-app-macros

cargo add syn quote proc-macro2 darling
</code></pre>
<h2 id="heading-how-to-write-a-simple-derive-macro">如何编写一个简单的派生宏</h2>
<p>在本节中，你将学习如何编写一个 <code>Derive</code> 宏。到现在为止，你应该已经了解了不同类型的宏及其含义，因为我们在前面的部分中已经讨论过它们。</p>
<h3 id="heading-the-intostringhashmap-derive-macro"><code>IntoStringHashMap</code> 派生宏</h3>
<p>假设你有一个应用程序，你需要能够将结构体转换为使用 <code>String</code> 类型作为键和值的哈希映射。这意味着它应该适用于所有字段都可以使用 <code>Into</code> 特性转换为 <code>String</code> 类型的任何结构体。</p>
<h3 id="heading-how-to-declare-a-derive-macro">如何声明一个派生宏</h3>
<p>你通过创建一个函数并使用属性宏注解该函数来声明宏，这些属性宏告诉编译器将该函数视为宏声明。由于你的 <code>lib.rs</code> 现在是空的，你还需要将 <code>proc-macro2</code> 声明为外部 crate:</p>
<pre><code class="language-rust">// my-app-macros/src/lib.rs
extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro_derive(IntoStringHashMap)]
pub fn derive_into_hash_map(item: TokenStream) -&gt; TokenStream {
    todo!()
}
</code></pre>
<p>我们在这里所做的只是将我们的宏声明为具有标识符 <code>IntoStringHashMap</code> 的派生宏。注意，这里的函数名称并不重要。重要的是传递给 <code>proc_macro_derive</code> 属性宏的标识符。</p>
<p>让我们先看看你可以如何使用它 —— 我们稍后再来实现它：</p>
<pre><code class="language-rust">// my-app/src/main.rs

use my_app_macros::IntoStringHashMap;

#[derive(IntoStringHashMap)]
pub struct User {
    username: String,
    first_name: String,
    last_name: String,
    age: u32,
}

fn main() {

}
</code></pre>
<p>通过你为它声明的标识符（在本例中是 <code>IntoStringHashMap</code>），你可以像使用任何其他派生宏一样使用你的宏。</p>
<p>如果你在此阶段尝试编译代码，你应该会看到以下编译错误：</p>
<pre><code>   Compiling my-app v0.1.0 

error: proc-macro derive panicked
 --&gt; src/main.rs:3:10
  |
3 | #[derive(IntoHashMap)]
  |          ^^^^^^^^^^^
  |
  = help: message: not yet implemented

error: could not compile `my-app` (bin "my-app") due to 1 previous error
</code></pre>
<p>这清楚地证明了我们的宏在编译阶段被执行了，因为，编译阶段触发了 panic 错误 <code>help: message: not yet implemented</code>，这正是 <code>todo!()</code> 宏所做的事。</p>
<p>这意味着我们的宏声明和其用法都有效。接下来，我们现在来实际实现这个宏了。</p>
<h3 id="how-to-parse-macro-input">如何解析宏输入</h3>
<p>首先，你使用 <code>syn</code> 将输入 token 流解析为 <code>DeriveInput</code>，这是任何可以使用派生宏的目标的表示：</p>
<pre><code class="language-rust">let input = syn::parse_macro_input!(item as syn::DeriveInput);
</code></pre>
<p><code>syn</code> 为我们提供了 <code>parse_macro_input</code> 宏，它使用一种自定义语法作为其参数。你为它提供输入变量的名称，<code>as</code> 关键字，以及输入 token 流应被解析为的 <code>syn</code> 中的数据类型（在我们的例子中是 <code>DeriveInput</code>）。</p>
<p>如果你查看 <code>DeriveInput</code> 的源代码，你会看到它给了我们以下信息：</p>
<ul>
<li><code>attrs</code>：应用到此类型的属性，无论是我们声明的其他属性宏还是内置的，例如 <code>must_use</code>。</li>
<li><code>vis</code>：此类型声明的可见性说明符。</li>
<li><code>ident</code>：类型的标识符（名称）。</li>
<li><code>generics</code>：此类型采用的泛型参数的信息，包括生命周期。</li>
<li><code>data</code>：一个枚举，描述目标是结构体、枚举还是联合体，并向我们提供更多相关信息。</li>
</ul>
<p>这些字段名称及其类型（除了 <code>data</code> 字段）在 <code>syn</code> 支持的目标中相当标准，如函数、枚举等。</p>
<p>如果进一步查看 <code>Data</code> 枚举的声明，特别是 <code>DataStruct</code>，你会看到它为你提供了一个名为 <code>fields</code> 的字段。这是此结构体所有字段的集合，你可以用它来遍历它们。这正是我们构建哈希映射所需要的！</p>
<p>这个宏的完整实现如下：</p>
<pre><code class="language-rust">// my-app/my-app-macros/lib.rs

extern crate proc_macro2;

use proc_macro::TokenStream;
use quote::quote;
use syn::Data;

#[proc_macro_derive(IntoHashMap)]
pub fn into_hash_map(item: TokenStream) -&gt; TokenStream {
    let input = syn::parse_macro_input!(item as syn::DeriveInput);

    let struct_identifier = &amp;input.ident;

    match &amp;input.data {
        Data::Struct(syn::DataStruct { fields, .. }) =&gt; {
            let mut implementation = quote!{
                let mut hash_map = std::collections::HashMap::&lt;String, String&gt;::new();
            };

            for field in fields {
                let identifier = field.ident.as_ref().unwrap();
                implementation.extend(quote!{
                    hash_map.insert(stringify!(#identifier).to_string(), String::from(value.#identifier));
                });
            }

            quote! {
                #[automatically_derived]
                impl From&lt;#struct_identifier&gt; for std::collections::HashMap&lt;String, String&gt; {
                    fn from(value: #struct_identifier) -&gt; Self {
                        #implementation

                        hash_map
                    }
                }
            }
        }
        _ =&gt; unimplemented!()
    }.into()
}
</code></pre>
<p>这里发生了很多事情，让我们分解一下:</p>
<h3 id="how-to-ensure-a-struct-target-for-macro">如何确保宏的目标是一个结构体</h3>
<p><code>let struct_identifier = &amp;input.ident;</code>：你将结构体标识符存储在一个单独的变量中，这样你以后就可以轻松使用它。</p>
<pre><code class="language-rust">match &amp;input.data {
    Data::struct(syn::DataStruct { fields, .. }) =&gt; { ... },
    _ =&gt; unimplemented!()
}
</code></pre>
<p>你在 <code>DeriveInput</code> 的解析数据字段上进行匹配。如果它是 <code>DataStruct</code> 类型（一个 Rust 结构体），则继续，否则触发 panic 错误，因为宏尚未为其他类型实现。</p>
<h3 id="heading-how-to-build-the-output-code">如何构建输出代码</h3>
<p>让我们看看当目标类型为 <code>DataStruct</code> 时，匹配分支的实现：</p>
<pre><code class="language-rust">let mut implementation = quote!{
    let mut hash_map = std::collections::HashMap::&lt;String, String&gt;::new();
};
</code></pre>
<p>在这里，你使用 <code>quote</code> 创建了一个新的 <code>TokenStream</code>。这个 <code>TokenStream</code> 与标准库提供的不同，不要与之混淆。它需要是可变的，因为我们很快会向这个 <code>TokenStream</code> 添加更多代码。</p>
<p><code>TokenStream</code> 基本上是 AST 的逆表示。你将实际的 Rust 代码提供给 <code>quote</code> 宏，它会给我们之前称之为的“token 流”。</p>
<p>这个 <code>TokenStream</code> 要么可以转换为宏的输出类型，要么可以使用 <code>quote</code> 提供的方法进行操作，例如 <code>extend</code>。</p>
<p>让我们继续，</p>
<pre><code class="language-rust">for field in fields {
    let identifier = field.ident.as_ref().unwrap();
    implementation.extend(quote!{
        hash_map.insert(
            stringify!(#identifier).to_string(),
            String::from(value.#identifier)
        );
    });
}
</code></pre>
<p>你遍历所有字段。在每次迭代中，你首先创建一个变量 <code>identifier</code> 来保存字段的名称以便以后使用。然后你使用 <code>extend</code> 方法在我们之前创建的 <code>TokenStream</code> 上添加额外的代码。</p>
<p><code>extend</code> 方法接受另一个 <code>TokenStream</code> 作为输入，这可以很容易地使用 <code>quote</code> 宏生成。对于要扩展的代码，你只需要编写代码将一个新条目插入将在宏输出中创建的 <code>hash_map</code>。</p>
<p>让我们仔细看看：</p>
<pre><code class="language-rust">hash_map.insert(
    stringify!(#identifier).to_string(),
    String::from(value.#identifier)
);
</code></pre>
<p>你知道，insert 方法需要一个键和值。你已告知编译器，键和值都是 <code>String</code> 类型。<code>stringify</code> 是标准库中的一个内置宏，可将任何 <code>Ident</code> 类型转换为其 <code>&amp;str</code> 等效项。你在这里使用它将字段标识符转换为实际的 <code>&amp;str</code>。然后你调用 <code>to_string()</code> 方法将其转换为 <code>String</code> 类型。</p>
<p>但是 <code>#identifier</code> 代表什么？</p>
<p><code>quote</code> 为你提供了在 <code>TokenStream</code> 中使用任何在其外部声明的变量的能力，使用 <code>#</code> 前缀。可以将其视为 format 参数中的 <code>{}</code>。此情况下，<code>#identifier</code> 简单地替换为我们在 <code>extend</code> 调用之外声明的字段标识符。因此，你实际上是直接在字段标识符上调用 <code>stringify!()</code> 宏。</p>
<p>同样，你可以使用熟悉的 <code>struct_variable.field_name</code> 语法来访问字段的值，但使用标识符变量代替字段名称。这就是你在 insert 语句中传递该值时所做的：<code>String::from(value.#identifier)</code>。</p>
<p>如果你仔细看代码，你会意识到 <code>value</code> 从何而来，但如果没有，它只是 trait 实现方法在进一步声明其输入参数时使用的。</p>
<p>一旦你使用 for 循环为结构体中的每个字段构建了实现，你就有了一个 <code>TokenStream</code>，在上面的例子中，它包含以下代码：</p>
<pre><code class="language-rust">let mut hash_map = std::collections::HashMap::&lt;String, String&gt;::new();
hash_map.insert("username".to_string(), String::from(value.username));
hash_map.insert("first_name".to_string(), String::from(value.first_name));
hash_map.insert("last_name".to_string(), String::from(value.last_name));
</code></pre>
<p>继续生成我们的宏的输出，你可以看到：</p>
<pre><code class="language-rust">quote! {
    impl From&lt;#struct_identifier&gt; for std::collections::HashMap&lt;String, String&gt; {
        fn from(value: #struct_identifier) -&gt; Self {
            #implementation

            hash_map
        }
    }
}
</code></pre>
<p>这里，你首先使用 <code>quote</code> 创建另一个 <code>TokenStream</code>。你在这个代码块中编写你的 <code>From</code> 特性实现。</p>
<p>接下来的这一行再次使用我们刚刚看到的带 <code>#</code> 前缀的语法，通过填入结构体的标识符，你声明了特性实现应该基于你的目标结构体。在这种情况下，如果你将派生宏应用于 <code>User</code> 结构体，这个标识符将被替换为 <code>User</code>。</p>
<pre><code class="language-rust">impl From&lt;#struct_identifier&gt; for std::collections::HashMap&lt;String, String&gt; {}
</code></pre>
<p>最后，实际的方法体如下：</p>
<pre><code class="language-rust">fn from(value: #struct_identifier) -&gt; Self {
    #implementation

    hash_map
}
</code></pre>
<p>如你所见，你可以使用相同的 <code>#</code> 语法轻松地将一个 <code>TokenStream</code> 嵌套到另一个 <code>TokenStream</code> 中，这种语法允许你在 <code>quote</code> 宏中使用外部变量。</p>
<p>在这里，你声明你的哈希映射实现应插入函数的前几行。然后你简单地返回同一个 <code>hash_map</code>。这完成了你的特性实现。</p>
<p>作为最后一步，你在 <code>match</code> 块的返回类型上调用 <code>.into()</code>，它返回 <code>quote</code> 宏调用的输出。这将 <code>quote</code> 中的 <code>TokenStream</code> 类型转换为标准库中的 <code>TokenStream</code> 类型，并由编译器预期从宏返回。</p>
<p>如果我逐行分解时理解起来比较困难，你可以查看下面的完整但带注释的代码：</p>
<pre><code class="language-rust">// 告诉编译器这个函数是一个派生宏，而派生的标识符是 `IntoHashMap`。
#[proc_macro_derive(IntoHashMap)]
// 声明一个函数，该函数接收一个输入 `TokenStream` 并输出 `TokenStream`。
pub fn into_hash_map(item: TokenStream) -&gt; TokenStream {
    // 将输入的 token stream 解析为 `syn` 库提供的 `DeriveInput` 类型。
    let input = syn::parse_macro_input!(item as syn::DeriveInput);

    // 将结构体标识符（名称）存储到一个变量中，以便你可以将其插入到输出代码中。
    let struct_identifier = &amp;input.ident;

    // 对应用了派生宏的目标类型进行匹配
    match &amp;input.data {
        // 匹配目标是一个结构体，并从它的信息中解构 `fields` 字段。
        Data::Struct(syn::DataStruct { fields, .. }) =&gt; {
            // 声明一个新的 quote 块，它将保存你的哈希映射实现的代码。
            // 这个块将既创建一个新的哈希映射，也将用结构体中的所有字段填充它。
            let mut implementation = quote!{
                // 这是你希望在输出中看到的代码。在这种情况下，你希望创建一个新的哈希映射。
                let mut hash_map = std::collections::HashMap::&lt;String, String&gt;::new();
            };

            // 遍历目标结构体的所有字段
            for field in fields {
                // 创建一个变量来存储字段的标识符（名称），以备后用
                let identifier = field.ident.as_ref().unwrap();
                // 扩展你的 `implementation` 块，以便在输出中包含用当前字段的信息填充创建的哈希映射。
                implementation.extend(quote!{
                    // 使用 `stringify!` 宏将字段标识符转换为字符串。这将作为你新哈希映射条目的键。
                    // 对于这个键的值，我们使用 `value.#identifier` 访问结构体中的字段值，
                    // 其中 `#identifier` 在输出代码中替换为实际的字段名。
                    hash_map.insert(stringify!(#identifier).to_string(), String::from(value.#identifier));
                });
            }

            // 创建最终输出块
            quote! {
                // 实现 `From` 特性，以允许将你的目标结构体标识为 `struct_identifier` 转换为 
                // 键和值均为 `String` 的 HashMap。
                // 就像先前一样，`#struct_identifier` 在输出代码中被替换为目标结构体的实际名称。
                impl From&lt;#struct_identifier&gt; for std::collections::HashMap&lt;String, String&gt; {
                    // `From` 特性要求你实现的一个方法。
                    // 输入值的类型再次为 `#struct_identifier`，在输出代码中被替换为目标结构体的名称。
                    fn from(value: #struct_identifier) -&gt; Self {
                        // 使用 `quote!` 将你创建的 `implementation` 块包含在这个方法体中。
                        // `quote` 允许你自由嵌套其他的 `quote` 块。
                        #implementation

                        // 返回 hash_map。
                        hash_map
                    }
                }
            }
        }
        // 如果目标类型是任何其他类型，则触发 panic 错误。
        _ =&gt; unimplemented!()
        // 将 `quote` 使用的 `TokenStream` 类型转换为标准库和编译器使用的 `TokenStream` 类型。
    }.into()
}
</code></pre>
<p>就是这样。你现在写好了你的第一个 Rust 过程宏！</p>
<p><strong>是时候享受你劳动的成果了。</strong></p>
<h3 id="heading-how-to-use-your-derive-macro">如何使用你的派生宏</h3>
<p>回到你的 <code>my-app/main.rs</code> 文件中，让我们调试打印一下你使用宏创建的哈希表。你的 <code>main.rs</code> 应该看起来像这样：</p>
<pre><code class="language-rust">// my-app/src/main.rs

use std::collections::HashMap;
use my_app_macros::IntoHashMap;

#[derive(IntoHashMap)]
pub struct User {
    username: String,
    first_name: String,
    last_name: String,
}

fn main() {
    let user = User {
        username: "username".to_string(),
        first_name: "First".to_string(),
        last_name: "Last".to_string(),
    };

    let hash_map = HashMap::&lt;String, String&gt;::from(user);

    dbg!(hash_map);
}
</code></pre>
<p>如果你使用 <code>cargo run</code> 运行这个程序，你应该会在终端上看到以下输出：</p>
<pre><code>[src/main.rs:20:5] hash_map = {
    "last_name": "Last",
    "first_name": "First",
    "username": "username",
}
</code></pre>
<p>就是这样！</p>
<h3 id="heading-how-to-improve-our-implementation">如何改进我们的实现</h3>
<p>在原始实现中，我有意跳过了一种更好地使用迭代器和 <code>quote</code> 的方式，因为这能促使我们学习更多 <code>quote</code> 特有的语法。</p>
<p>让我们看看使用这种方式会是怎样的，然后再深入了解它的工作原理：</p>
<pre><code class="language-rust">let input = syn::parse_macro_input!(item as syn::DeriveInput);
    let struct_identifier = &amp;input.ident;

    match &amp;input.data {
        Data::Struct(syn::DataStruct { fields, .. }) =&gt; {
            let field_identifiers = fields.iter().map(|item| item.ident.as_ref().unwrap()).collect::&lt;Vec&lt;_&gt;&gt;();

            quote! {
                impl From&lt;#struct_identifier&gt; for std::collections::HashMap&lt;String, String&gt; {
                    fn from(value: #struct_identifier) -&gt; Self {
                        let mut hash_map = std::collections::HashMap::&lt;String, String&gt;::new();

                        #(
                            hash_map.insert(stringify!(#field_identifiers).to_string(), String::from(value.#field_identifiers));
                        )*

                        hash_map
                    }
                }
            }
        }
        _ =&gt; unimplemented!()
    }.into()
</code></pre>
<p>这看起来更加简洁易懂！让我们看看使这一切成为可能的特殊语法 – 特别是以下这一行：</p>
<pre><code class="language-rust">#(
    hash_map.insert(stringify!(#field_identifiers).to_string(), String::from(value.#field_identifiers));
)*
</code></pre>
<p>我们来分解一下。首先，将整个代码块包裹在 <code>#()*</code> 中，代码将放在括号内。这种语法允许你在括号内使用任何迭代器，并且它会为迭代器中的每个项目重复该代码块，同时在每次迭代中用正确的项目替换变量。</p>
<p>在这种情况下，你首先创建一个 <code>field_identifiers</code> 迭代器，这是目标结构体中所有字段标识符的集合。然后你为迭代器中的每个项目编写 <code>hash_map</code> 插入语句。<code>#()*</code> 包装器将其转换为预期的多行输出，每行对应迭代器中的一个项目。</p>
<h2 id="heading-a-more-elaborate-derive-macro">更复杂的派生宏</h2>
<p>现在你已经熟悉如何编写简单的 Derive 宏，是时候进一步创建一个在实际场景中更有用的宏了，特别是当你处理数据库模型时。</p>
<h3 id="heading-the-derivecustommodel-macro"><code>DeriveCustomModel</code> 宏</h3>
<p>你将要构建一个派生宏，帮助你从原始结构体生成派生结构体。在处理数据库时，你会经常需要这个，尤其是当你只想加载部分数据时。</p>
<p>例如，如果你有一个包含所有用户信息的 <code>User</code> 结构体，但你只想从数据库加载用户的姓名信息，你就需要一个只包含这些字段的结构体 – 除非你想让所有字段都成为 Option 类型，但这不是一个好主意。</p>
<p>我们还需要添加 <code>From</code> trait 的实现，以便能够自动从 <code>User</code> 结构体转换为派生结构体。我们的宏还需要能够从同一个目标结构体派生多个模型。</p>
<p>让我们先在 <code>lib.rs</code> 中声明它：</p>
<pre><code class="language-rust">// lib.rs

#[proc_macro_derive(DeriveCustomModel, attributes(custom_model))]
pub fn derive_custom_model(item: TokenStream) -&gt; TokenStream {
    todo!()
}
</code></pre>
<p>大部分语法你应该已经从我们之前的例子中熟悉了。唯一的增加部分是我们现在还在 <code>proc_macro_derive</code> 调用中定义了 <code>attributes(custom_model)</code>，这基本上告诉编译器将任何以 <code>#[custom_model]</code> 开头的属性视为此派生宏在该目标上的参数。</p>
<p>例如，一旦你定义了这个，你可以在目标结构体上应用 <code>#[custom_model(name = "SomeName")]</code>，以定义派生结构体应具有的名称 "SomeName"。你需要自己解析并处理它，当然 – 这个定义只是告诉编译器将其传递给你的宏实现，而不要将其视为未知属性。</p>
<p>我们还需要创建一个新文件来包含此宏的实现细节。宏规则规定它需要在 <code>lib.rs</code> 中<strong>定义</strong>，我们已经做到了。实现本身可以放在项目中的任何地方。</p>
<p>让我们创建 <code>custom_model.rs</code> 文件：</p>
<pre><code class="language-shell">touch src/custom_model.rs
</code></pre>
<h3 id="how-to-separate-implementation-from-declaration">如何将实现与声明分离</h3>
<p>定义一个实现 <code>DeriveCustomModel</code> 宏的函数。我们还将立即添加所有的导入，以避免后续的混淆：</p>
<pre><code class="language-rust">// custom_model.rs

use syn::{
    parse_macro_input, Data::Struct, DataStruct, DeriveInput, Field, Fields, Ident, Path,
};
use darling::util::PathList;
use darling::{FromAttributes, FromDeriveInput, FromMeta};
use proc_macro::TokenStream;
use quote::{quote, ToTokens};

pub(crate) fn derive_custom_model_impl(input: TokenStream) -&gt; TokenStream {
    // 将输入的 token 流解析为 `DeriveInput`
    let original_struct = parse_macro_input!(input as DeriveInput);

    // 从输入中解构出 data 和 ident 字段
    let DeriveInput { data, ident, .. } = original_struct.clone();
}
</code></pre>
<p>这只是一个 Rust 函数，所以这里没有特殊的规则。你可以像调用常规 Rust 函数那样从声明中调用它。</p>
<pre><code class="language-rust">#[proc_macro_derive(DeriveCustomModel, attributes(custom_model))]
pub fn derive_custom_model(item: TokenStream) -&gt; TokenStream {
    custom_model::custom_model_impl(item)
}
</code></pre>
<h3 id="heading-how-to-parse-derive-macro-arguments">如何解析派生宏参数</h3>
<p>要解析我们的派生宏的参数（通常是通过应用于目标或其字段的属性提供的参数），我们将使用 <code>darling</code> 库，使其像定义数据类型一样简单。</p>
<pre><code class="language-rust">// custom_model.rs

// 为此结构派生 `FromDeriveInput`，该宏由 darling 提供，
// 能够自动添加将参数 token 解析到给定结构中的功能。
#[derive(FromDeriveInput, Clone)]
// 我们告诉 darling，我们正在查找使用 `custom_model` 
// 属性定义的参数，并且我们只支持命名结构。
#[darling(attributes(custom_model), supports(struct_named))]
struct CustomModelArgs {
    // 指定生成派生模型的参数。
    // 通过为每个模型重复此属性，可以生成多个模型。
    #[darling(default, multiple, rename = "model")]
    pub models: Vec&lt;CustomModel&gt;,
}
</code></pre>
<p>我们告诉 <code>darling</code>，对于结构的参数，我们应该期待一个 <code>model</code> 参数列表，每个参数将为一个派生模型定义参数。这使我们可以使用宏从单个输入结构生成多个派生结构。</p>
<p>接下来，让我们定义每个模型的参数：</p>
<pre><code class="language-rust">// custom_model.rs

// 为此结构派生 `FromMeta`，该宏由 darling 提供，
// 能够自动添加将元数据解析到给定结构中的功能。
#[derive(FromMeta, Clone)]
struct CustomModel {
    // 生成模型的名称。
    name: String,
    // 逗号分隔的字段标识符列表，
    // 这些字段将包含在生成的模型中。
    fields: PathList,
    // 应对生成的结构应用的额外的派生列表，例如 `Eq` 或 `Hash`。
    #[darling(default)]
    extra_derives: PathList,
}
</code></pre>
<p>在这个结构中，我们有两个必需的参数：<code>name</code> 和 <code>fields</code>，以及一个可选的参数 <code>extra_derives</code>。由于在它上面有 <code>#[darling(default)]</code> 注解，它是可选的。</p>
<h3 id="heading-how-to-implement-derivecustommodel">如何实现 <code>DeriveCustomModel</code></h3>
<p>现在我们已经定义了所有的数据类型，让我们开始解析——这就像调用我们的参数结构上的一个方法一样简单！完整的函数实现看起来应该像这样：</p>
<pre><code class="language-rust">// custom_model.rs

pub(crate) fn derive_custom_model_impl(input: TokenStream) -&gt; TokenStream {
    // 将输入的 token 流解析为 `DeriveInput`
    let original_struct = parse_macro_input!(input as DeriveInput);

    // 从输入中解构出 data 和 ident 字段
    let DeriveInput { data, ident, .. } = original_struct.clone();

    if let Struct(data_struct) = data {
        // 从这个数据结构中提取字段
        let DataStruct { fields, .. } = data_struct;

        // `darling` 在结构上提供了这个方法让我们方便地解析参数，
        // 并且还能为我们处理错误。
        let args = match CustomModelArgs::from_derive_input(&amp;original_struct) {
            Ok(v) =&gt; v,
            Err(e) =&gt; {
                // 如果 darling 返回了一个错误，则生成一个
                // token 流，从而使编译器在正确的位置显示错误。
                return TokenStream::from(e.write_errors());
            }
        };

        // 从解析的参数中解构 `models` 字段。
        let CustomModelArgs { models } = args;

        // 创建一个新的输出
        let mut output = quote!();

        // 如果没有定义模型但使用了宏，则触发 panic 错误。
        if models.is_empty() {
            panic!(
                "请使用 `model` 属性至少指定1个模型"
            )
        }

        // 迭代所有定义的模型
        for model in models {
            // 根据目标结构的字段和 `model` 参数生成自定义模型。
            let generated_model = generate_custom_model(&amp;fields, &amp;model);

            // 扩展输出以包含生成的模型
            output.extend(quote!(#generated_model));
        }

        // 将输出转换为 TokenStream 并返回
        output.into()
    } else {
        // 如果目标不是命名结构，则触发 panic 错误
        panic!("DeriveCustomModel 只能用于命名结构")
    }
}
</code></pre>
<p>生成每个模型的 token 的代码已被抽取到我们称之为 <code>generate_custom_model</code> 的另一个函数中。我们也来实现这个函数：</p>
<h3 id="heading-how-to-generate-each-custom-model">如何生成每个自定义模型</h3>
<pre><code class="language-rust">fn generate_custom_model(fields: &amp;Fields, model: &amp;CustomModel) -&gt; proc_macro2::TokenStream {
    let CustomModel {
        name,
        fields: target_fields,
        extra_derives,
    } = model;

    // 创建用于作为输出的变量 new_fields
    let mut new_fields = quote!();

    // 遍历源结构体的所有字段
    for Field {
        // 该字段的标识符
        ident,
        // 该字段的属性
        attrs,
        // 该字段的可见性
        vis,
        // 分隔符 `:`
        colon_token,
        // 该字段的类型
        ty,
        ..
    } in fields
    {
        // 确保该字段有标识符，否则触发 panic 错误
        let Some(ident) = ident else {
            panic!("无法获取字段标识符")
        };

        // 尝试将字段标识符转换为 `Path`，这是由 `syn` 提供的一种类型。
        // 这样做是因为 `darling` 的 PathList 只是一个带有 Path 的集合，并有一些附加方法。
        let path = match Path::from_string(&amp;ident.clone().to_string()) {
            Ok(path) =&gt; path,
            Err(error) =&gt; panic!("无法将字段标识符转换为 path: {error:?}"),
        };

        // 如果目标字段列表不包含此字段，则跳过
        if !target_fields.contains(&amp;path) {
            continue;
        }

        // 如果包含，则重构字段声明，并将其添加到 `new_fields` 输出中，
        // 以便我们可以在输出结构中使用它。
        new_fields.extend(quote! {
            #(#attrs)*
            #vis #ident #colon_token #ty,
        });
    }

    // 创建一个新的标识符，用于输出结构的名称
    let struct_ident = match Ident::from_string(name) {
        Ok(ident) =&gt; ident,
        Err(error) =&gt; panic!("{error:?}"),
    };

    // 创建一个 TokenStream，用于保存额外的派生声明
    let mut extra_derives_output = quote!();

    // 如果 extra_derives 不为空，则将其添加到输出中
    if !extra_derives.is_empty() {
        // 这种语法有点紧凑，但你应该已经知道如何理解它。
        extra_derives_output.extend(quote! {
            #(#extra_derives,)*
        })
    }

    // 构造最终的结构体，将所有生成的 TokenStream 组合在一起。
    quote! {
        #[derive(#extra_derives_output)]
        pub struct #struct_ident {
            #new_fields
        }
    }
}
</code></pre>
<h3 id="how-to-use-your-derivecustommodal-macro">如何使用这个 <code>DeriveCustomModel</code> 宏</h3>
<p>回到你的 <code>my-app/main.rs</code>，让我们调试打印用你实现的宏创建的新结构体的哈希表。你的 <code>main.rs</code> 应该如下所示：</p>
<pre><code class="language-rust">// my-app/src/main.rs

use macros::{DeriveCustomModel, IntoStringHashMap};
use std::collections::HashMap;

#[derive(DeriveCustomModel)]
#[custom_model(model(
    name = "UserName",
    fields(first_name, last_name),
    extra_derives(IntoStringHashMap)
))]
#[custom_model(model(name = "UserInfo", fields(username, age), extra_derives(Debug)))]
pub struct User2 {
    username: String,
    first_name: String,
    last_name: String,
    age: u32,
}

fn main() {
    let user_name = UserName {
        first_name: "first_name".to_string(),
        last_name: "last_name".to_string(),
    };
    let hash_map = HashMap::&lt;String, String&gt;::from(user_name);

    dbg!(hash_map);

    let user_info = UserInfo {
        username: "username".to_string(),
        age: 27,
    };

    dbg!(user_info);
}
</code></pre>
<p>如你所见，<code>extra_derives</code> 对我们已经很有用了，因为我们需要为新模型派生 <code>Debug</code> 和 <code>IntoStringHashMap</code>。</p>
<p>如果你使用 <code>cargo run</code> 运行它，你应该在终端中看到以下输出：</p>
<pre><code>[src/main.rs:32:5] hash_map = {
    "last_name": "last_name",
    "first_name": "first_name",
}
[src/main.rs:39:5] user_info = UserInfo {
    username: "username",
    age: 27,
}
</code></pre>
<p>我们将在这里结束派生宏的部分。</p>
<h2 id="heading-a-simple-attribute-macro">一个简单的属性宏</h2>
<p>在本节中，你将学习如何编写一个<strong>属性</strong>宏。</p>
<h3 id="heading-the-logduration-attribute"><code>log_duration</code> 属性</h3>
<p>你将编写一个简单的属性宏，它可以应用于任何函数（或方法），并在每次调用函数时记录函数的总运行时间。</p>
<h3 id="heading-how-to-declare-an-attribute-macro">如何声明一个属性宏</h3>
<p>通过创建一个函数并使用 <code>proc_macro_attribute</code> 宏注解该函数来声明属性宏，该宏告诉编译器将该函数视为宏声明。让我们看看它是什么样的：</p>
<pre><code class="language-rust">// my-app-macros/src/lib.rs

#[proc_macro_attribute]
pub fn log_duration(args: TokenStream, item: TokenStream) -&gt; TokenStream {
    log_duration_impl(args, item)
}
</code></pre>
<p>对于这些宏，函数名称非常重要，因为它也成为宏的名称。如你所见，它们接受两个不同的参数。第一个是传递给属性宏的参数，第二个是属性宏的目标。</p>
<p>让我们也实现 <code>log_duration_impl</code>。创建一个新的文件 <code>log_duration.rs</code>：</p>
<pre><code class="language-shell">touch src/log_duration.rs
</code></pre>
<h3 id="heading-how-to-implement-the-logduration-attribute-macro">如何实现 <code>log_duration</code> 属性宏</h3>
<p>我将首先为您提供完整的实现，然后我会分解一些我之前没有使用的部分：</p>
<pre><code class="language-rust">// my-app-macros/src/log_duration.rs

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

pub(crate) fn log_duration_impl(_args: TokenStream, input: TokenStream) -&gt; TokenStream {
    // 将输入解析为 `ItemFn`，这是 `syn` 提供的一种表示函数的类型。
    let input = parse_macro_input!(input as ItemFn);

    let ItemFn {
        // 函数签名
        sig,
        // 该函数的可见性说明符
        vis,
        // 函数体
        block,
        // 应用于此函数的其他属性
        attrs,
    } = input;

    // 提取函数体中的语句
    let statements = block.stmts;

    // 存储用于日志记录的函数标识符
    let function_identifier = sig.ident.clone();

    // 使用解析的输入重新构建函数作为输出
    quote!(
        // 重新应用此函数上的所有其他属性。
        // 编译器不会在此列表中包含我们当前正在处理的宏。
        #(#attrs)*
        // 重新构建函数声明
        #vis #sig {
            // 在函数开始时，创建一个 `Instant` 实例
            let __start = std::time::Instant::now();

            // 创建一个新的块，其主体是函数的主体。
            // 将此块的返回值存储为一个变量，以便我们之后可以从父函数中返回它。
            let __result = {
                #(#statements)*
            };

            // 记录此函数的持续时间信息
            println!("{} 耗时 {}μs", stringify!(#function_identifier), __start.elapsed().as_micros());

            // 返回结果（如果有的话）
            return __result;
        }
    )
    .into()
}
</code></pre>
<p>你之前可能没见过的唯一事情是 <code>sig</code> 和 <code>block</code> 字段，它们是通过将输入解析为 <code>ItemFn</code> 获得的。<code>sig</code> 包含函数的整个签名，而 <code>block</code> 包含函数的整个主体。这就是为什么，通过使用下面的代码，我们可以基本上重新构建未修改的函数：</p>
<pre><code class="language-rust">// 在宏中重新构建未修改的函数的示例代码

#vis #sig #block
</code></pre>
<p>在这个例子中，你需要修改函数体，这就是为什么你要创建一个新的块来封装原始函数块。</p>
<h3 id="how-to-use-your-log-duration-macro">如何使用这个 <code>log_duration</code> 宏</h3>
<p>回到 <code>main.rs</code>，使用属性宏比你想象的要简单：</p>
<pre><code class="language-rust">// main.rs

#[log_duration]
#[must_use]
fn function_to_benchmark() -&gt; u16 {
    let mut counter = 0;
    for _ in 0..u16::MAX {
        counter += 1;
    }

    counter
}

fn main() {
    println!("{}", function_to_benchmark());
}
</code></pre>
<p>当你运行这个程序时，你应该得到以下输出：</p>
<pre><code>function_to_benchmark 耗时 498μs
65535
</code></pre>
<p>我们现在准备好转向更复杂的用例。</p>
<h2 id="heading-a-more-elaborate-attribute-macro">一个更复杂的属性宏</h2>
<h3 id="heading-the-cachedfn-attribute"><code>cached_fn</code> 属性</h3>
<p>你将编写一个属性宏，它将允许你为任何函数添加缓存功能。对于这个示例，我们假设我们的函数总是具有 <code>String</code> 参数，并且也返回一个 <code>String</code> 值。</p>
<p>对这个概念，有些人可能更熟悉将其称为“记忆化”函数。</p>
<p>此外，你需要允许这个宏的用户告诉宏它如何基于函数参数生成一个动态键。</p>
<p>为了帮助我们实现缓存部分，以免被分散注意力，我们将使用一个名为 <code>cacache</code> 的依赖项。<code>cacache</code> 是一个 Rust 库，用于管理本地键和内容缓存。它通过将缓存写入磁盘来工作。</p>
<p>让我们通过直接编辑 <code>my-app</code> 的 <code>Cargo.toml</code> 文件来添加它到项目中：</p>
<pre><code class="language-toml">// Cargo.toml

workspace = { members = ["my-app-macros"] }

[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
resolver = "2"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
# 新依赖项
cacache = { version = "13.0.0", default-features = false, features = ["mmap"] }
macros = { path = "./macros" }
</code></pre>
<h3 id="heading-how-to-implement-the-cachedfn-attribute-macro">如何实现 <code>cached_fn</code> 属性宏</h3>
<p>让我们从 <code>lib.rs</code> 中声明这个宏开始：</p>
<pre><code class="language-rust">// my-app-macros/src/lib.rs

#[proc_macro_attribute]
pub fn cached_fn(args: TokenStream, item: TokenStream) -&gt; TokenStream {
    cached_fn_impl(args, item)
}
</code></pre>
<p>创建一个新的文件 <code>cached_fn.rs</code> 来存储实现：</p>
<pre><code class="language-shell">touch my-app-macros/src/cached_fn.rs
</code></pre>
<p>让我们在实现之前定义下我们的参数应该是什么样子的：</p>
<h3 id="heading-cachedfn-attribute-arguments"><code>cached_fn</code> 的属性参数</h3>
<pre><code class="language-rust">// my-app-macros/src/cached_fn.rs

#[derive(FromMeta)]
struct CachedParams {
    // 接受我们应该用来计算键的任意表达式。
    // 这可以是一个常量字符串，或者是基于函数参数的一些计算。
    keygen: Option&lt;Expr&gt;,
}
</code></pre>
<p>唯一的参数是一个可选的 <code>keygen</code>，其类型为 <code>Expr</code>。<code>Expr</code> 表示任何有效的 <a href="https://rustwiki.org/zh-CN/reference/expressions.html">Rust 表达式</a>，因此它可以非常灵活。在这个例子中，你将传递一个基于目标函数的参数生成键的表达式。</p>
<p>一如既往，我们先来看看整体实现，稍后再讲解新知识：</p>
<pre><code class="language-rust">// my-app-macros/src/cached_fn.rs

pub fn cached_fn_impl(args: TokenStream, item: TokenStream) -&gt; TokenStream {
    // 将参数 token 解析为 NestedMeta 项的列表
    let attr_args = match NestedMeta::parse_meta_list(args.into()) {
        Ok(v) =&gt; v,
        Err(e) =&gt; {
            // 如果有错误，将错误写入输出令牌流
            return proc_macro::TokenStream::from(Error::from(e).write_errors());
        }
    };

    // 将嵌套的元列表解析为我们的 `CachedParams` 结构体
    let CachedParams { keygen } = match CachedParams::from_list(&amp;attr_args) {
        Ok(params) =&gt; params,
        Err(error) =&gt; {
            // 如果有错误，将错误写入输出令牌流
            return proc_macro::TokenStream::from(Error::from(error).write_errors());
        }
    };

    // 将输入目标项目解析为一个函数
    let ItemFn {
        // 函数签名
        sig,
        // 函数的可见性说明符
        vis,
        // 函数块或主体
        block,
        // 其他应用于此函数的属性
        attrs,
    } = parse_macro_input!(item as ItemFn);

    // 根据给定的参数（或缺少参数）生成我们的键语句
    let key_statement = if let Some(keygen) = keygen {
        // 如果用户指定了 `keygen`，则将其用作获取缓存键的表达式。
        quote! {
            let __cache_key = #keygen;
        }
    } else {
        // 如果没有提供 `keygen`，则使用函数名称作为缓存键。
        let fn_name = sig.ident.clone().to_string();
        quote! {
            let __cache_key = #fn_name;
        }
    };

    // 使用解析的输入重新构造函数作为输出
    quote!(
        // 将原始函数的其他属性应用于生成的函数
        #(#attrs)*
        #vis #sig {
            // 在函数主体的第一件事中包含我们生成的 key_statement
            #key_statement

            // 尝试从缓存中读取值
            match cacache::read_sync("./__cache", __cache_key.clone()) {
                // 如果值存在，将其解析为字符串并返回
                Ok(value) =&gt; {
                    println!("缓存命中");
                    from_utf8(&amp;value).unwrap().to_string()
                },
                Err(_) =&gt; {
                    println!("缓存未命中");
                    // 将原始函数块的输出保存到变量中。
                    let output = #block;

                    // 将输出值以字节形式写入缓存
                    cacache::write_sync("./__cache", __cache_key, output.as_bytes()).unwrap();

                    // 返回原始输出
                    output
                }
            }
        }
    )
    .into()
}
</code></pre>
<p>好了，事实证明你已经看过了我们在这一节中使用的所有内容。</p>
<p>唯一新的东西是使用 <code>cacache</code> 依赖项，但这也相当简单。你只需提供要存储缓存数据的位置作为 <code>read_sync</code> 和 <code>write_sync</code> 函数的第一个参数。</p>
<p>我们还添加了一些日志记录来帮助我们验证宏是否按预期工作。</p>
<h3 id="heading-how-to-use-the-cachedfn-macro">如何使用 <code>cached_fn</code> 宏</h3>
<p>要将任何函数变为记忆化或缓存的，我们只需使用 <code>cached_fn</code> 属性对其进行注释：</p>
<pre><code class="language-rust">// src/main.rs

#[cached_fn(keygen = "format!(\"{first_name} {last_name}\")")]
fn test_cache(first_name: String, last_name: String) -&gt; String {
    format!("{first_name} {last_name}")
}

fn main() {
    test_cache("John".to_string(), "Appleseed".to_string());
    test_cache("John".to_string(), "Appleseed".to_string());
    test_cache("John".to_string(), "Doe".to_string());
}
</code></pre>
<p>如果运行这个，你应该会看到以下输出：</p>
<pre><code>缓存未命中
缓存命中
缓存未命中
</code></pre>
<p>这清楚地表明，如果函数对相同的参数调用多次，则从缓存中返回数据。但如果参数不同，则不会返回为不同参数集缓存的值。</p>
<p>我们为此做了很多不适用于现实世界的假设。因此，这只是为了学习目的，但描绘了一个真实世界的用例。</p>
<p>例如，我编写了属性宏来使用 <code>redis</code> 缓存 HTTP 处理函数，以用于生产服务器。它们的实现与此非常相似，但包含许多特性以适应特定用例。</p>
<h2 id="heading-a-simple-function-like-macro">一个简单的函数式宏</h2>
<p>现在终于可以再次享受一些 <em>乐趣</em> 了。我们将从简单的开始，但第二个示例将包含解析自定义语法。<em>非常有趣</em>，对吧？</p>
<p>免责声明：如果你熟悉声明式宏（使用 <code>macro_rules!</code> 语法），你可能会意识到以下示例可以轻松地使用该语法编写，并且不需要过程宏。要想写出简单但是无法用声明性宏实现的过程宏是非常困难的，尽管如此，我们还是选择了以下示例。</p>
<h3 id="heading-the-constantstring-macro"><code>constant_string</code> 宏</h3>
<p>我们将构建一个非常简单的宏，它将一个字符串字面量（类型为 <code>&amp;str</code>）作为输入，并为其创建一个全局公共常量（变量名称与值相同）。基本上，我们的宏将生成以下内容：</p>
<pre><code class="language-rust">pub const STRING_LITERAL: &amp;str = "STRING_LITERAL";
</code></pre>
<h3 id="heading-how-to-declare-a-function-like-macro">如何声明一个类函数的宏</h3>
<p>你可以通过创建一个函数并使用 <code>proc_macro</code> 宏注解该函数来声明类函数的宏。它告诉编译器将该函数视为宏声明。让我们看看这是什么样子的：</p>
<pre><code class="language-rust">// my-app-macros/src/lib.rs

#[proc_macro]
pub fn constant_string(item: TokenStream) -&gt; TokenStream {
    constant_string_impl(item)
}
</code></pre>
<p>对于这些宏，函数名称非常重要，因为它也成为宏的名称。如你所见，这些宏只接受一个参数，即你传递给宏的内容。它可以是任何东西，甚至是无效的 Rust 代码的自定义语法。</p>
<h3 id="heading-how-to-implement-the-constantstring-macro">如何实现 <code>constant_string</code> 宏</h3>
<p>对于实现，让我们创建一个新的文件<code>constant_string.rs</code>：</p>
<pre><code class="language-shell">touch my-app-macros/src/constant_string.rs
</code></pre>
<p>实现非常简单：</p>
<pre><code class="language-rust">use darling::FromMeta;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Ident, LitStr};

pub fn constant_string_impl(item: TokenStream) -&gt; TokenStream {
    // 将输入解析为字符串字面量
    let constant_value = parse_macro_input!(item as LitStr);

    // 从传递的字符串值创建一个新的 `Ident`（标识符）。
    // 这将成为常量变量的名称。
    let constant_value_name = Ident::from_string(&amp;constant_value.value()).unwrap();

    // 生成声明常量变量的代码。
    quote!(pub const #constant_value_name: &amp;str = #constant_value;).into()
}
</code></pre>
<p>我们所做的只是将输入解析为字符串字面量。如果你传递的内容不是字符串字面量，它将触发一个错误。然后我们获取字符串，创建一个标识符，并生成输出代码。简短且简单。</p>
<h3 id="heading-how-to-use-the-constantstring-macro">如何使用 <code>constant_string</code> 宏</h3>
<p>使用此宏也非常简单：</p>
<pre><code class="language-rust">// src/main.rs

constant_string!("SOME_CONSTANT_STRING_VALUE");
</code></pre>
<p>上面的代码将展开为：</p>
<pre><code class="language-rust">pub const SOME_CONSTANT_STRING_VALUE: &amp;str = "SOME_CONSTANT_STRING_VALUE";
</code></pre>
<h2 id="heading-a-more-elaborate-function-like-macro">更复杂的类函数宏</h2>
<p>顾名思义，类函数宏可以类似于调用函数的方式使用。你还可以在任何可以调用函数的地方使用它们，以及其他地方。</p>
<h3 id="heading-the-hashmapify-macro"><code>hash_mapify</code> 宏</h3>
<p>进入有趣的部分：你现在将编写的宏将允许你通过简单地传递一组键值对来生成一个 <code>HashMap</code>。例如：</p>
<pre><code class="language-rust">let variable = "Some variable";

hash_mapify!(
    &amp;str,
    key = "value",
    key2 = "value2",
    key3 = "value3",
    key4 = variable
);
</code></pre>
<p>如你所见，我们希望第一个参数是值的类型，后续参数是键值对。我们需要自己解析所有这些内容。</p>
<p>为了简化处理，因为这个过程很容易变得复杂，我们只支持字符串、整数、浮点数和布尔值等基本类型。因此，我们不支持创建非字符串键或具有枚举和结构体值的<code>hash_map</code>。</p>
<h3 id="heading-how-to-implement-the-hashmapify-macro">如何实现 <code>hash_mapify</code> 宏</h3>
<p>我们将像往常一样开始声明宏：</p>
<pre><code class="language-rust">// my-app-macros/src/lib.rs

#[proc_macro]
pub fn hash_mapify(item: TokenStream) -&gt; TokenStream {
    hash_mapify_impl(item)
}
</code></pre>
<p>接下来，你需要定义一个数据结构来保存输入数据。在这种情况下，你需要知道传递的值类型，以及一组键值对。</p>
<p>我们将实现部分提取到一个单独的文件，在那里你还将实现数据类型和解析逻辑。</p>
<p>创建新文件 <code>hash_mapify.rs</code> 并声明保存输入数据的数据类型：</p>
<pre><code class="language-shell">touch my-app-macros/src/hash_mapify.rs
</code></pre>
<h3 id="how-to-parse-hash-mapifys-input">如何解析 <code>hash_mapify</code> 的输入</h3>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, Lit, LitStr, Token, Type};

pub struct ParsedMapEntry(String, proc_macro2::TokenStream);

pub struct ParsedMap {
    value_type: Type,
    entries: Vec&lt;ParsedMapEntry&gt;,
}
</code></pre>
<p>你直接以 <code>TokenStream</code> 类型保存值，因为你需要同时支持字面值和变量，这两者在此上下文中只有一个共同类型 <code>TokenStream</code>。</p>
<p>你可能还注意到，我们将 <code>value_type</code> 保存为 <code>Type</code>，这是 <code>syn</code> 库提供的一种类型，它是 Rust 值可能具有的类型的枚举。这真是满满的干货！</p>
<p>你不需要处理每个枚举变体，因为这种类型也可以直接转换为 <code>TokenStream</code>。你很快就会更好地理解这意味着什么。</p>
<p>下一步，你需要为之前声明的 <code>ParsedMap</code> 实现 <code>syn::parse::Parse</code> trait，以便可以从传递给宏的<code>TokenStream</code> 中计算它。</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        let mut entries = Vec::&lt;ParsedMapEntry&gt;::new();
    }
}
</code></pre>
<p><code>input</code>（在这个例子中类型为<code>ParsedStream</code>）的工作方式类似于迭代器。你需要使用其上的方法 <code>parse</code> 解析出输入的 token，这也会将流推进到下一个 token 的开头。</p>
<p>例如，如果你有一个表示 <code>[a, b, c]</code> 的 token 流，当你从这个流中解析出 <code>[</code> 时，该流将被改变为仅包含<code>a, b, c]</code>。这非常类似于迭代器，一旦你从中取出一个值，迭代器就会前进一个位置，只保留剩余的项。</p>
<p>在你解析任何内容之前，你需要检查输入是否为空，如果为空，则会触发 panic 错误：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        // ...

        // 检查输入是否为空（没有传递任何参数）。
        // 如果为空，则触发 panic 错误，因为我们无法继续进行。
        if input.is_empty() {
            panic!("至少需要为一个空的hashmap指定一个类型");
        }

        // ...
    }
}
</code></pre>
<p>由于我们预计传递给宏的第一个参数是我们的hashmap中值的类型，让我们从 token 流中解析出来：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        // ...

        // 由于第一个参数应该是`Type`类型，你可以尝试
        // 从输入中解析出`Type`，否则返回一个错误。
        let ty = input.parse::&lt;Type&gt;()?;

        // ...
    }
}
</code></pre>
<p><code>Parse</code> 接受一个表示要解析内容的单一类型参数。</p>
<p>如果第一个参数无法解析为有效类型，将返回一个错误。请注意，这不会验证你传递的类型是否实际存在，它只会验证第一个参数中的 token 是否适合类型定义，仅此而已。</p>
<p>这意味着如果你传递<code>SomeRandomType</code>，而<code>SomeRandomType</code>实际上并没有定义，解析仍然会成功。只有在编译时扩展宏时，才会失败。</p>
<p>接下来，我们还希望用户使用 <code>,</code> 来分隔参数。让我们将其解析为类型之后的下一个 token：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        // ...

        // 下一步，解析 `,` token，你期望它被用来分隔参数。
        input.parse::&lt;Token![,]&gt;()?;

        // ...
    }
}
</code></pre>
<p>你可能会注意到，当为 <code>parse</code> 方法提供类型参数时，使用了 <code>Token!</code> 宏。这是 <code>syn</code> 提供的一个宏，用于轻松转换内置类型，比如关键字（<code>type</code>，<code>async</code>，<code>fn</code> 等），标点符号（<code>,</code>，<code>.</code>，<code>;</code> 等）以及分隔符（<code>{</code>，<code>[</code>，<code>(</code> 等）。此宏接受一个参数，即需要类型的关键字/标点符号/分隔符字面量。</p>
<p>官方文档将其定义为：</p>
<blockquote>
<p>一个可扩展为给定 token 的 Rust 类型表示的名称的类型宏。</p>
</blockquote>
<p>现在你有了值的类型以及第一个分隔符（逗号），是时候开始解析键值对了。所有的键值对都遵循相同的结构<code>key = value</code>，并由逗号分隔。</p>
<p>请注意，空白不是重点，因为它完全在分词（tokenization，译者注：将代码文本分割成 token 的过程）过程中处理，不是你需要处理的内容。</p>
<p>由于你不知道传递了多少键值对，你需要某些方法来告诉你什么时候解析完成：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        // ...

        // 循环直到输入为空（没有剩余的内容可以解析）。
        while !input.is_empty() {
            // ..
        }

        // ...
    }
}
</code></pre>
<p>如我之前所述，token 是从流中取出的，并在每次你解析某些内容时前移。这意味着当所有 token 都解析完毕时，流将为空。我们在这里利用这一事实来确定何时跳出循环。</p>
<p>每个键值对的解析方式类似于你解析类型参数的方式：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        // ...

        // 循环直到输入为空（没有剩余的内容可以解析）。
        while !input.is_empty() {
            // 尝试将键解析为标识符
            let key = if let Ok(key) = input.parse::&lt;syn::Ident&gt;() {
                key.to_string()
                // 如果它不是标识符，则尝试将其解析为字符串字面量
            } else if let Ok(key) = input.parse::&lt;LitStr&gt;() {
                key.value()
                // 如果它既不是标识符也不是字符串字面量，
                // 则它不是有效的键，因此触发适当的 panic 错误。
            } else {
                panic!("键必须是字符串字面量或标识符！");
            };

            // 将解析的键值对推入我们的列表。
            entries.push(ParsedMapEntry(key, value));

            // 检查下一个 token 是否是逗号，不提前推进流
            if input.peek(Token![,]) {
                // 如果是的话，先将其解析，然后在继续解析下一个键值对之前推进流
                input.parse::&lt;Token![,]&gt;()?;
            }
        }

    // ...
    }
}
</code></pre>
<p>这里唯一新增的是最后对 <code>peek</code> 方法的调用。这是一个特殊的方法，如果传递给 <code>peek</code> 的 token 是流中的下一个 token，则返回 true，否则返回 false。</p>
<p>正如名字所示，这只执行检查，所以它不会将该 token 从流中取出或以任何形式推进流。</p>
<p>一旦所有解析完成，只需作为之前声明的 <code>ParsedMap</code> 结构体的一部分返回信息。如果如下的完整实现更便于你阅读，可以参考：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        let mut entries = Vec::&lt;ParsedMapEntry&gt;::new();

        // 检查输入是否为空（没有传递参数）。
        // 如果为空，则触发错误，因为我们无法继续。
        if input.is_empty() {
            panic!("至少必须为空的 hashmap 指定一个类型");
        }

        // 因为第一个参数应该是 `Type` 类型，尝试从输入中解析 `Type`，否则返回错误。
        let ty = input.parse::&lt;Type&gt;()?;

        // 接下来，解析 `,` token，你期望用它来分隔参数。
        input.parse::&lt;Token![,]&gt;()?;

        // 循环直到输入为空（没有剩下的东西可解析）。
        while !input.is_empty() {
            // 尝试解析键为标识符
            let key = if let Ok(key) = input.parse::&lt;syn::Ident&gt;() {
                key.to_string()
                // 如果不是标识符，尝试解析为字符串字面量
            } else if let Ok(key) = input.parse::&lt;LitStr&gt;() {
                key.value()
                // 如果既不是标识符也不是字符串字面量，
                // 则不是有效的键，触发适当错误。
            } else {
                panic!("键必须是字符串字面量或标识符！");
            };

            // 解析 `=` 符号，它应该是键后的下一个 token。
            input.parse::&lt;Token![=]&gt;()?;

            // 接下来，尝试将值解析为标识符。
            // 如果解析出标识符，表示它是一个变量，所以我们应直接将其转换为 token 流。
            let value = if let Ok(value) = input.parse::&lt;syn::Ident&gt;() {
                value.to_token_stream()
                // 如果输入不是标识符，尝试将其解析为字面量值，
                // 如 `"string"` 是字符串， `42` 是数字，`false` 是布尔值等。
            } else if let Ok(value) = input.parse::&lt;Lit&gt;() {
                value.to_token_stream()
            } else {
                // 如果输入既不是标识符也不是字面量值，则触发适当的错误。
                panic!("值必须是字面量或标识符！");
            };

            // 将解析的键值对推入我们的列表。
            entries.push(ParsedMapEntry(key, value));

            // 检查下一个 token 是否为逗号，不推进流
            if input.peek(Token![,]) {
                // 如果是，则将其解析出来并推进流
                // 之后再解析下一个键值对。
                input.parse::&lt;Token![,]&gt;()?;
            }
        }

        Ok(ParsedMap {
            value_type: ty,
            entries,
        })
    }
}
</code></pre>
<h3 id="how-to-generate-output-code">如何生成输出代码</h3>
<p>现在你终于可以编写实际的宏实现了，这会是相当直接的：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

pub fn hash_mapify_impl(item: TokenStream) -&gt; TokenStream {
    // 将输入 token 流解析为我们定义的 `ParsedMap`。
    // 这会使用我们之前实现的解析 trait 的逻辑。
    let input = parse_macro_input!(item as ParsedMap);

    let key_value_pairs = input.entries;
    let ty = input.value_type;

    // 在代码块内生成输出的 hashmap 以避免与现有变量冲突。
    // 从块中返回 hashmap。
    quote!({
        // 创建一个新的 hashmap，其键类型为 `String`，值类型为从宏输入参数中解析的 `#ty`。
        let mut hash_map = std::collections::HashMap::&lt;String, #ty&gt;::new();

        // 将所有键值对插入 hashmap。
        #(
            hash_map.insert(#key_value_pairs);
        )*

        // 返回生成的 hashmap
        hash_map
    })
    .into()
}
</code></pre>
<p>如果你一路跟着这篇文章编写代码，或者有一双敏锐的眼睛，你可能会注意到这里有一个错误。变量 <code>key_value_pairs</code> 的类型是 <code>Vec&lt;ParsedMapEntry&gt;</code>。我们试图在输出中使用它：</p>
<pre><code class="language-rust">#(hash_map.insert(#key_value_pairs);)*
</code></pre>
<p>这是使用列表的正确语法，但底层类型 <code>ParsedMapEntry</code> 是自定义类型。<code>syn</code> 和 <code>quote</code> 都不知道如何将其转换为 token 流。因此，我们无法使用此语法。</p>
<p>但是，如果我们尝试手动编写实现，在其中遍历自己，在每个循环中生成单独的 token 流，并扩展现有的 token 流，将会非常繁琐。是否有更好的解决方案呢？确实有：<code>ToTokens</code> trait。</p>
<h3 id="heading-how-to-convert-custom-data-types-to-output-tokens">如何将自定义数据类型转换为输出 token</h3>
<p>这个 trait 可以为我们的任何自定义类型实现，并定义类型在转换为 token 流时的样子。</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

impl ToTokens for ParsedMapEntry {
    fn to_tokens(&amp;self, tokens: &amp;mut proc_macro2::TokenStream) {
        let key = self.0.clone();
        let value = self.1.clone();

        tokens.extend(quote!(String::from(#key), #value));
    }
}
</code></pre>
<p>作为实现的一部分，你需要修改 <code>tokens</code> 参数并扩展它，以包含我们希望类型生成的 token 流。我用来实现这一点的语法现在应该都很熟悉了。</p>
<p>一旦完成了这一点，<code>quote</code> 现在可以轻松地将有问题的代码转换为 token 流。因此，这个：<code>#(hash_map.insert(#key_value_pairs);)*</code> 现在将可以工作。</p>
<p>像往常一样，我放上完整实现的代码，希望能帮你更容易理解：</p>
<pre><code class="language-rust">// my-app-macros/src/hash_mapify.rs

use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, Lit, LitStr, Token, Type};

pub struct ParsedMapEntry(String, proc_macro2::TokenStream);

pub struct ParsedMap {
    value_type: Type,
    entries: Vec&lt;ParsedMapEntry&gt;,
}

impl ToTokens for ParsedMapEntry {
    fn to_tokens(&amp;self, tokens: &amp;mut proc_macro2::TokenStream) {
        let key = self.0.clone();
        let value = self.1.clone();

        tokens.extend(quote!(String::from(#key), #value));
    }
}

impl Parse for ParsedMap {
    fn parse(input: ParseStream) -&gt; syn::Result&lt;Self&gt; {
        let mut entries = Vec::&lt;ParsedMapEntry&gt;::new();

        // 检查输入是否为空（没有参数传递）。
        // 如果不是，则报错，因为我们无法继续下去。
        if input.is_empty() {
            panic!("至少要为一个空的 hashmap 指定一个类型");
        }

        // 由于第一个参数应该是 `Type` 类型，所以你需要
        // 从输入中解析出 `Type`，否则返回错误。
        let ty = input.parse::&lt;Type&gt;()?;

        // 解析 `,` token，你期望它用于分隔参数。
        input.parse::&lt;Token![,]&gt;()?;

        // 循环，直到输入为空（没有其他东西可以解析）。
        while !input.is_empty() {
            // 尝试解析键作为标识符
            let key = if let Ok(key) = input.parse::&lt;syn::Ident&gt;() {
                key.to_string()
                // 如果不是标识符，尝试解析它作为字符串字面量
            } else if let Ok(key) = input.parse::&lt;LitStr&gt;() {
                key.value()
                // 如果既不是标识符也不是字符串字面量，
                // 则它不是有效的键，所以报错。
            } else {
                panic!("键必须是字符串字面量或标识符！");
            };

            // 解析 `=` 符号，它应该是键之后的下一个 token。
            input.parse::&lt;Token![=]&gt;()?;

            // 接下来，尝试解析值作为标识符。
            // 如果是，则说明它是一个变量，所以我们应该直接转换为 token 流。
            let value = if let Ok(value) = input.parse::&lt;syn::Ident&gt;() {
                value.to_token_stream()
                // 如果输入不是标识符，尝试解析它作为字面值，
                // 比如 `"string"` 是字符串，`42` 是数字，`false` 是布尔值等。
            } else if let Ok(value) = input.parse::&lt;Lit&gt;() {
                value.to_token_stream()
            } else {
                // 如果输入既不是标识符，也不是字面值，则报错。
                panic!("值必须是字面量或标识符！");
            };

            // 将解析出的键值对添加到我们的列表中。
            entries.push(ParsedMapEntry(key, value));

            // 检查下一个 token 是否是逗号，而不推进流
            if input.peek(Token![,]) {
                // 如果是，则解析出它并推进流，然后继续处理下一个键值对
                input.parse::&lt;Token![,]&gt;()?;
            }
        }

        Ok(ParsedMap {
            value_type: ty,
            entries,
        })
    }
}

pub fn hash_mapify_impl(item: TokenStream) -&gt; TokenStream {
    // 解析输入 token 流为我们定义的 `ParsedMap`。
    // 这将使用我们之前实现的解析 trait 逻辑。
    let input = parse_macro_input!(item as ParsedMap);

    let key_value_pairs = input.entries;
    let ty = input.value_type;

    // 在代码块中生成输出的哈希表，这样我们就不会影射任何现有的变量。返回代码块中的哈希表。
    quote!({
        // 用 `String` 作为键类型，并使用从宏输入参数中解析的 `#ty` 作为值类型来创建一个新的哈希映射。
        let mut hash_map = std::collections::HashMap::&lt;String, #ty&gt;::new();

        // 将所有键值对插入哈希表。
        #(
            hash_map.insert(#key_value_pairs);
        )*

        // 返回生成的哈希表
        hash_map
    })
    .into()
}
</code></pre>
<h3 id="heading-how-to-use-the-hashmapify-macro">如何使用 <code>hash_mapify</code> 宏</h3>
<p>我们可以通过编写一个简单的用例来验证我们的宏是否有效：</p>
<pre><code class="language-rust">// src/main.rs

fn main() {
    test_hashmap();
}

fn test_hashmap() {
    let some_variable = "Some variable value";

    let hash_map = hash_mapify!(
        &amp;str,
        "first_key" = "first_value",
        "second_variable" = some_variable,
        some_key = "value for variable key",
    );

    let number_hash_map =
        hash_mapify!(usize, "first_key" = 1, "second_variable" = 2, some_key = 3,);

    dbg!(hash_map);
    dbg!(number_hash_map);
}
</code></pre>
<p>如果你运行这段代码，你应该会看到以下输出：</p>
<pre><code>[src/main.rs:62:5] hash_map = {
    "first_key": "first_value",
    "some_key": "value for variable key",
    "second_variable": "Some variable value",
}
[src/main.rs:63:5] number_hash_map = {
    "second_variable": 2,
    "first_key": 1,
    "some_key": 3,
}
</code></pre>
<p>这正是我们希望看到的结果。</p>
<p>现在我们已经涵盖了所有三种类型的过程宏，我们将在此处结束示例。</p>
<h2 id="heading-beyond-writing-macros">编写宏 —— 更进一步</h2>
<p>既然你已经学会了如何编写基本的派生宏，我想花点时间快速介绍一些在处理宏时很有帮助的工具和技术。我还会指出一些为什么以及何时避免使用它们的缺点。</p>
<h3 id="heading-helpful-cratestools">有用的库/工具</h3>
<p><a href="https://github.com/dtolnay/cargo-expand"><strong>cargo-expand</strong></a></p>
<p>这是一个 CLI 工具，可以为项目中的任何文件生成宏扩展代码。另一个由 <a href="https://crates.io/users/dtolnay">David Tolnay</a> 发起的伟大项目。不过，使用这个工具需要 Rust 的 nightly 工具链。别担心 —— 这只需要工具本身工作。你不需要让你的项目使用 nightly 工具链。你的项目可以继续使用稳定版。</p>
<p>安装 nightly 工具链：</p>
<pre><code class="language-shell">rustup toolchain install nightly
</code></pre>
<p>安装 <code>cargo-expand</code>：</p>
<pre><code class="language-shell">cargo install cargo-expand
</code></pre>
<p>现在已经完成了，你可以看到 main 中代码的实际扩展。只需在 <code>my-app</code> 项目目录中运行以下命令：</p>
<pre><code class="language-shell">cargo expand
</code></pre>
<p>它将在终端输出中输出扩展代码。你会看到一些不熟悉的东西，比如 <code>dbg!</code> 宏的扩展，但你可以忽略这些。</p>
<p><strong><a href="https://docs.rs/trybuild/latest/trybuild/#">trybuild</a> 和 <a href="https://docs.rs/macrotest/latest/macrotest/#">macrotest</a></strong></p>
<p>如果你想单元测试你的过程宏的扩展形式或断言任何预期的编译错误，这两个库非常有用。</p>
<h2 id="heading-downsides-of-macros">宏的缺点</h2>
<h3 id="heading-debugging-or-lack-thereof">调试（或者说缺乏调试）</h3>
<p>你不能在由宏生成的代码的任何行中设置断点。在错误的栈追踪中，你也无法到达它。这使得调试生成的代码变得非常困难。</p>
<p>在我的通常工作流程中，我要么将日志记录添加到生成的代码中，要么如果这还不够，我会暂时用 <code>cargo expand</code> 给我的代码替换掉宏的用法来调试，进行更改，然后基于此更新宏代码。</p>
<p>可能还有更好的方法，如果你知道任何方法并愿意分享给我，我将不胜感激。</p>
<h3 id="heading-compile-time-costs">编译时成本</h3>
<p>编译器运行和处理宏扩展并非零成本，编译器随后还需要检查它生成的代码是否有效。当涉及递归宏时，开销更大。</p>
<p>作为一个非常粗略的估算，每个宏扩展为项目的编译时间增加 10 毫秒。如果你感兴趣，我鼓励你阅读这篇关于编译器如何内部处理宏的<a href="https://rustc-dev-guide.rust-lang.org/macro-expansion.html">入门介绍</a>。</p>
<h3 id="heading-lack-of-auto-complete-and-code-checks">缺乏自动补全和代码检查</h3>
<p>目前，作为宏输出部分编写的代码未完全由任何 IDE 支持，也未由 rust-analyzer 支持。因此，在大多数情况下，你是在不依赖于自动完成、自动建议等功能的情况下编写代码。</p>
<h3 id="heading-where-do-we-draw-the-line">我们应该止步于何处？</h3>
<p>鉴于宏的无限潜力，很容易在使用它们时迷失。重要的是要记住所有的缺点，并相应地做出决定，确保你不会沉溺于提前的抽象。</p>
<p>作为一般规则，我个人避免使用宏来实现任何“业务逻辑”，也不尝试编写宏来生成需要反复调试的代码。或者是需要进行微小变更以进行性能测试和改进的代码。</p>
<h2 id="heading-wrapping-up">总结</h2>
<p>这是一段很长的旅程！但我希望任何具有基本 Rust 知识和经验的人都能跟上，并在此之后能够在自己的项目中编写宏。</p>
<p>你可以在 <a href="https://github.com/anshulsanghi-blog/macros-handbook">https://github.com/anshulsanghi-blog/macros-handbook</a> 仓库中找到本文中所提到的所有代码。</p>
<p>另外，如果你有任何问题或对本主题有任何意见，欢迎**<a href="mailto:contact@anshulsanghi.tech">联系我</a>**。</p>
<h3 id="heading-enjoying-my-work">喜欢我的作品吗？</h3>
<p>考虑请我喝杯咖啡来支持我的工作吧！</p>
<p><a href="https://buymeacoffee.com/anshulsanghi">☕请我喝杯咖啡</a></p>
<p>下次再见，祝你编程愉快，天空晴朗！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何用 OpenAPI 在 Express 中构建更好的 API ]]>
                </title>
                <description>
                    <![CDATA[ 我将在这篇文章中分享在 Express 中构建强大的 REST API 的方法。首先，我会介绍构建 REST API 的一些挑战，然后提出一个使用开放标准的解决方案。 本文并非一篇关于 Node.js [https://nodejs.org/en/]、Express.js [https://expressjs.com/] 或  REST API [/news/rest-apis/] 的介绍。如果你需要复习，请在深入研究本文内容之前查看这些链接。🤿 我喜欢 Node.js 那极具灵活性和易用性的生态。这个社区充满活力，并且你可以用你已经掌握的语言在几分钟内设置一个 REST API。 在应用的前后端使用相同的编程语言是一件很有价值的事。这使我们在浏览代码库时可以减少上下文切换 [https://blog.rescuetime.com/context-switching/]，从而变得更轻松。全栈开发者可以快速切换技术栈，共享代码 [https://betterprogramming.pub/sharing-logic-components-between-frontend-and- ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-build-explicit-apis-with-openapi/</link>
                <guid isPermaLink="false">641fbc4ee32a7606487d581c</guid>
                
                    <category>
                        <![CDATA[ API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Wed, 29 Mar 2023 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/03/6065d95e9618b008528ab4a6.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-build-explicit-apis-with-openapi/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Build Better APIs in Express with OpenAPI</a>
      </p><!--kg-card-begin: markdown--><p>我将在这篇文章中分享在 Express 中构建强大的 REST API 的方法。首先，我会介绍构建 REST API 的一些挑战，然后提出一个使用开放标准的解决方案。</p>
<p>本文并非一篇关于 <a href="https://nodejs.org/en/">Node.js</a>、<a href="https://expressjs.com/">Express.js</a> 或 <a href="https://chinese.freecodecamp.org/news/rest-apis/">REST API</a> 的介绍。如果你需要复习，请在深入研究本文内容之前查看这些链接。🤿</p>
<p>我喜欢 Node.js 那极具灵活性和易用性的生态。这个社区充满活力，并且你可以用你已经掌握的语言在几分钟内设置一个 REST API。</p>
<p>在应用的前后端使用相同的编程语言是一件很有价值的事。这使我们在浏览代码库时可以减少<a href="https://blog.rescuetime.com/context-switching/">上下文切换</a>，从而变得更轻松。全栈开发者可以快速切换技术栈，<a href="https://betterprogramming.pub/sharing-logic-components-between-frontend-and-backend-repositories-6fdc1f9cb850">共享代码</a>也变得轻而易举。</p>
<p>尽管如此，随着 MVP 成长为成熟的生产环境应用程序和开发团队规模的扩大，这种灵活性也带来了挑战。</p>
<h2 id="restapi">使用 REST API 的挑战</h2>
<p>无论你使用哪种技术栈，当代码库和团队规模增长时，都会面临许多挑战。</p>
<p>在本文中，我将描述通过 REST API 暴露业务逻辑的 Express.js 应用程序所带来的挑战，以小见大。</p>
<p>无论 API 消费者的性质如何（网页、移动应用、第三方后端），随着他们的成长，他们都可能面临以下一个（或多个）挑战：</p>
<h3 id="1">1. ⚠️ 更难做出改变</h3>
<p>在文档不够明确时，在 REST API 的任何一方进行修改都变得更加困难。</p>
<p>举个例子，假设你有一个 REST 端点，可以返回一个特定的用户的名字。在即将新增的功能中，你可能需要修改这个 API 使其返回年龄。这可能会潜在地破坏网络应用和移动应用。</p>
<p>你可以设置集成测试来一定程度上避免这个问题，但你仍然会严重依赖开发人员来手动覆盖所有的边界情况。这需要大量的时间和精力，而且你永远无法 100% 确定这些变化不会破坏应用程序。</p>
<h3 id="2">2. 📜 缺少（及时更新的）文档</h3>
<p>文档是构建 REST API 时的另一个敏感话题。我坚信在大多数情况下，代码本身应该足以代替一部分文档。</p>
<p>也就是说，REST API 在开发中会变得越来越复杂，检查代码中每个端点的安全性、参数和可能的响应也随之变得繁琐且耗时。这就减慢了开发的速度，也给 bug 进入系统留下了隐患。</p>
<p>即使团队致力于在一个独立于代码的文档中手动保持文档的更新，也很难 100% 确保它反映了代码的情况。</p>
<h3 id="3api">3. 📢 公共 API</h3>
<p>这并不适用于所有的应用程序，但在某些情况下，一个应用程序可能需要向第三方暴露一系列的功能。对于这种情况，第三方有可能会在我们暴露的 API 之上构建核心功能。</p>
<p>这意味着我们不能以更新我们的私有 API 的同样速度来修改这些公共 API。一旦修改了公共 API，第三方应用程序可能会因此崩溃，而这正是我们应该不惜一切代价避免的事情。</p>
<p>公共 API 所暴露的内容应该是明确的，并且可以简单地进行开发，以限制内部和外部开发团队之间所需的来回沟通的数量。</p>
<h3 id="4">4. ✍️ 手动集成测试</h3>
<p>当应用程序的开发没有与之匹配的周密计划时，很有可能 API 所提供的内容和 API 消费者期望的内容被深埋在代码中。</p>
<p>对于仅有少量的内部端点的系统来说，这并不是一个大问题。但随着 API 接口数量的增长，修改现有的端点需要在整个系统中遵循面包屑，以确保消费者期望得到的东西与提供的东西是相等的。</p>
<p>这个问题可以通过对系统的不同部分之间进行集成测试来缓解。但是人工完成这件事的工作量非常巨大的，并且如果没做好的话，可能会在系统实际上不能正常工作的时候让开发人员误以为系统状态良好。</p>
<h2 id="">提出的解决方案</h2>
<p>我们已经看到了构建 REST API 所带来的固有挑战。在下一节中，我们将使用开放标准构建一个示例 Express 项目，以解决这些挑战。</p>
<h3 id="api">API 标准规范</h3>
<p>前面部分描述的挑战已经存在很长时间了，所以面对这个问题，我们最好查看现有的解决方案，而不是重新发明轮子。</p>
<p>许多标准尝试对 REST API 进行规范化定义（<a href="https://raml.org/">RAML</a>、<a href="https://jsonapi.org/">JsonAPI</a>、<a href="https://www.openapis.org/">OpenAPI</a>......）。这些项目的共同目标是使开发人员更容易定义他们的 API 行为，以便跨多种语言的服务器和客户端能够“共说一种语言”。</p>
<p>有了某种形式的 API 规范，可以解决许多挑战，因为在许多情况下，可以从这些规范自动生成客户端 SDK、测试、模拟服务器和文档。</p>
<p>一种我最喜欢的规范是 OpenAPI（原名 Swagger）。它有一个很大的社区，并且有很多用于 Express 的工具。这可能不是所有 REST API 项目中的最佳工具，因此请在为你自己的项目选择规范之前进行额外的研究，以确保该规范的工具和支持对你的项目有帮助。</p>
<h3 id="">示例的背景</h3>
<p>在这个示例中，假设我们正在构建一个待办事项列表管理应用。用户可以通过访问一个 web 应用来获取、创建、编辑和删除待办事项，这些待办事项被保存在后端。</p>
<p>在这个例子中，后端使用一个 Express.js 应用程序，它将通过 REST API 暴露以下功能：</p>
<ul>
<li>获取待办事项: <strong>[GET] /todos</strong></li>
<li>创建待办事项：<strong>[POST] /todos</strong></li>
<li>编辑待办事项：<strong>[PUT] /todos/:id</strong></li>
<li>删除待办事项：<strong>[DELETE] /todos/:id</strong></li>
</ul>
<p>对于一个真实的待办事项管理应用来说，上面的功能有点过度简化，但这有助于展示我们如何在实际情况下克服上面提出的挑战。</p>
<h3 id="">实现</h3>
<p>很好，现在我们已经介绍了 API 定义的开放标准和背景，让我们来实现一个 Express 待办事项应用，演示怎么解决前面的挑战。</p>
<p>我们将使用 Express 库 <a href="https://github.com/kogosoftwarellc/open-api/tree/master/packages/express-openapi"><strong>express-openapi</strong></a> 的 OpenAPI。请注意，这个库提供的高级功能（响应验证、认证、中间件设置......）超出了本文的范围。</p>
<p>你可以在<a href="https://github.com/aperkaz/express-open-api">这个仓库</a>中找到演示的完整代码。</p>
<ol>
<li>初始化一个 Express 框架，并初始化一个 Git 仓库：</li>
</ol>
<pre><code class="language-bash">npx express-generator --no-view --git todo-app
cd ./todo-app
git init
git add .; git commit -m "Initial commit";
</code></pre>
<ol start="2">
<li>将 <strong><a href="https://github.com/kogosoftwarellc/open-api/tree/master/packages/express-openapi">express-openapi</a></strong> 引入我们的程序：</li>
</ol>
<p><code>npm i express-openapi -s</code></p>
<pre><code class="language-javascript">// ./app.js

...

app.listen(3030);

...

// OpenAPI routes
initialize({
  app,
  apiDoc: require("./api/api-doc"),
  paths: "./api/paths",
});

module.exports = app;
</code></pre>
<ol start="3">
<li>添加 OpenAPI 基础模型。</li>
</ol>
<p>请注意，模型中定义了 <strong>Todo</strong> 的类型，将在路由处理程序中引用。</p>
<pre><code class="language-javascript">// ./api/api-doc.js

const apiDoc = {
  swagger: "2.0",
  basePath: "/",
  info: {
    title: "Todo app API.",
    version: "1.0.0",
  },
  definitions: {
    Todo: {
      type: "object",
      properties: {
        id: {
          type: "number",
        },
        message: {
          type: "string",
        },
      },
      required: ["id", "message"],
    },
  },
  paths: {},
};

module.exports = apiDoc;
</code></pre>
<ol start="4">
<li>添加路由<a href="https://github.com/kogosoftwarellc/open-api/tree/master/packages/express-openapi#getting-started">处理程序</a>。</li>
</ol>
<p>每个处理程序都声明它支持哪些操作（GET、POST ...），对每个操作的回调，以及该处理程序的 <strong>apiDoc</strong> OpenAPI 模型。</p>
<pre><code class="language-javascript">// ./api/paths/todos/index.js
module.exports = function () {
  let operations = {
    GET,
    POST,
    PUT,
    DELETE,
  };

  function GET(req, res, next) {
    res.status(200).json([
      { id: 0, message: "First todo" },
      { id: 1, message: "Second todo" },
    ]);
  }

  function POST(req, res, next) {
    console.log(`About to create todo: ${JSON.stringify(req.body)}`);
    res.status(201).send();
  }

  function PUT(req, res, next) {
    console.log(`About to update todo id: ${req.query.id}`);
    res.status(200).send();
  }

  function DELETE(req, res, next) {
    console.log(`About to delete todo id: ${req.query.id}`);
    res.status(200).send();
  }

  GET.apiDoc = {
    summary: "Fetch todos.",
    operationId: "getTodos",
    responses: {
      200: {
        description: "List of todos.",
        schema: {
          type: "array",
          items: {
            $ref: "#/definitions/Todo",
          },
        },
      },
    },
  };

  POST.apiDoc = {
    summary: "Create todo.",
    operationId: "createTodo",
    consumes: ["application/json"],
    parameters: [
      {
        in: "body",
        name: "todo",
        schema: {
          $ref: "#/definitions/Todo",
        },
      },
    ],
    responses: {
      201: {
        description: "Created",
      },
    },
  };

  PUT.apiDoc = {
    summary: "Update todo.",
    operationId: "updateTodo",
    parameters: [
      {
        in: "query",
        name: "id",
        required: true,
        type: "string",
      },
      {
        in: "body",
        name: "todo",
        schema: {
          $ref: "#/definitions/Todo",
        },
      },
    ],
    responses: {
      200: {
        description: "Updated ok",
      },
    },
  };

  DELETE.apiDoc = {
    summary: "Delete todo.",
    operationId: "deleteTodo",
    consumes: ["application/json"],
    parameters: [
      {
        in: "query",
        name: "id",
        required: true,
        type: "string",
      },
    ],
    responses: {
      200: {
        description: "Delete",
      },
    },
  };

  return operations;
};
</code></pre>
<ol start="5">
<li>添加自动生成的文档，<strong><a href="https://github.com/scottie1984/swagger-ui-express">swagger-ui-express</a></strong>：</li>
</ol>
<pre><code class="language-bash">npm i swagger-ui-express -s
</code></pre>
<pre><code class="language-javascript">// ./app.js

...

// OpenAPI UI
app.use(
  "/api-documentation",
  swaggerUi.serve,
  swaggerUi.setup(null, {
    swaggerOptions: {
      url: "http://localhost:3030/api-docs",
    },
  })
);

module.exports = app;
</code></pre>
<p>这就是我们最终获得的效果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-23.png" alt="image-23" width="600" height="400" loading="lazy"></p>
<p>这个 SwaggerUi 是自动生成的，你可以在 <a href="http://localhost:3030/api-documentation">http://localhost:3030/api-documentation</a> 访问它。</p>
<p>🎉 <strong>恭喜！</strong></p>
<p>当你进行到文章的这里时，你应该创建好了一个完全可运行的 Express 应用程序，其与 OpenAPI 完全集成。</p>
<p>现在，通过使用在 <em><a href="http://localhost:3030/api-docs">http://localhost:3030/api-docs</a></em> 中定义的模型，我们可以轻松生成<a href="https://nordicapis.com/generating-web-api-tests-from-an-openapi-specification/">测试</a>、<a href="https://github.com/stoplightio/prism">模拟服务器</a>、<a href="https://github.com/drwpow/openapi-typescript">类型</a>，甚至<a href="https://phrase.com/blog/posts/using-openapi-to-generate-api-client-code/">客户端</a>！</p>
<h2 id="">总结</h2>
<p>我们只是浅浅涉猎了 OpenAPI 所能做到的事情。但是我希望这篇文章能够让你了解标准 API 定义模式是如何在可见性、测试、文档和整体置信度方面帮助构建 REST API 的。</p>
<p>谢谢你看到最后！</p>
<p>我目前正在构建 <a href="https://taggr.ai/"><strong><strong>taggr</strong></strong></a>，这是一个跨平台的桌面应用程序，它在帮助用户<strong>重新发现</strong>他们的数字记忆的同时保持他们的<strong>隐私</strong>。</p>
<p>Linux、Windows 和 macOS 平台上的 alpha 版本即将推出。请查看<a href="https://taggr.ai/">网页</a>并<a href="https://taggr.us18.list-manage.com/subscribe/post?u=482d473aa1e4dedadc89fb3e2&amp;id=aa6a10c164">登记</a>，以免错过！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 通过建立 CI/CD 管道来学习 Jenkins ]]>
                </title>
                <description>
                    <![CDATA[ Jenkins 是一个开源的自动化服务器，它使我们能够更加轻松地构建、测试和部署软件。 我们刚刚在 freeCodeCamp.org 的 YouTube 频道上发布了一个视频课程，它将通过展示如何为一个 Web 应用程序构建 CI/CD 管道来带你了解 Jenkins。 这门课程是由 Gwendolyn Faraday 开发的。Gwen 是一位经验丰富的软件开发者，她在自己和 freeCodeCamp 的频道上都有许多受欢迎的课程。 除了 Jenkins，本课程中的项目还使用了这些其他技术：  * 在 Linode 上运行的 Debian 服务器  * Docker 和 Dockerhub  * GitHub  * 一些用于设置的命令行 Jenkins 可以帮助开发者实现软件开发过程的自动化并提高他们的生产力。它还可以帮助用户更轻松地获取一份软件项目的全新构建。Jenkins 是创建 DevOps 管道的一个重要工具。 DevOps 管道是一套流程和工具，可以用它们来实现软件应用程序的持续交付。术语 “DevOps” 是“开发（development）”和“运营（opera ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-jenkins-by-building-a-ci-cd-pipeline/</link>
                <guid isPermaLink="false">63d63138634c950733e7f6a6</guid>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Thu, 26 Jan 2023 04:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/01/jenkins.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/learn-jenkins-by-building-a-ci-cd-pipeline/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Learn Jenkins by Building a CI/CD Pipeline – Full Course</a>
      </p><!--kg-card-begin: markdown--><p>Jenkins 是一个开源的自动化服务器，它使我们能够更加轻松地构建、测试和部署软件。</p>
<p>我们刚刚在 freeCodeCamp.org 的 YouTube 频道上发布了一个视频课程，它将通过展示如何为一个 Web 应用程序构建 CI/CD 管道来带你了解 Jenkins。</p>
<p>这门课程是由 Gwendolyn Faraday 开发的。Gwen 是一位经验丰富的软件开发者，她在自己和 freeCodeCamp 的频道上都有许多受欢迎的课程。</p>
<p>除了 Jenkins，本课程中的项目还使用了这些其他技术：</p>
<ul>
<li>在 Linode 上运行的 Debian 服务器</li>
<li>Docker 和 Dockerhub</li>
<li>GitHub</li>
<li>一些用于设置的命令行</li>
</ul>
<p>Jenkins 可以帮助开发者实现软件开发过程的自动化并提高他们的生产力。它还可以帮助用户更轻松地获取一份软件项目的全新构建。Jenkins 是创建 DevOps 管道的一个重要工具。</p>
<p>DevOps 管道是一套流程和工具，可以用它们来实现软件应用程序的持续交付。术语 “DevOps” 是“开发（development）”和“运营（operations）”两个词的组合。DevOps 管道被用作自动化进行软件开发生命周期中的构建、测试和部署阶段。</p>
<p>本课程中的项目的架构如下。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/09/image-372.png" alt="image-372" width="600" height="400" loading="lazy"></p>
<p>以下是本课程涉及的所有章节：</p>
<ul>
<li>课程概述</li>
<li>什么是 Jenkins？</li>
<li>术语和定义</li>
<li>项目架构</li>
<li>介绍 Linode</li>
<li>设置 Jenkins</li>
<li>浏览 Jenkins 界面</li>
<li>安装插件</li>
<li>Blue Ocean</li>
<li>创建一个管道</li>
<li>安装 Git</li>
<li>Jenkinsfile</li>
<li>更新管道</li>
<li>配合 npm 使用 Jenkins</li>
<li>Docker 和 Dockerhub</li>
<li>结束语</li>
</ul>
<p>请观看下方的完整版课程视频，你也可以在 <a href="https://www.youtube.com/watch?v=f4idgaq2VqA">freeCodeCamp.org 的 YouTube 频道</a>上找到这个视频（时长约一小时）。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何用 Markdown 语言写文章 ]]>
                </title>
                <description>
                    <![CDATA[ 作为一名开发者，你很可能听说过 HTML [https://baike.baidu.com/item/HTML/97049]，即超文本标记语言（HyperT ext Markup Language）。 你可能还知道 HTML 是一种用于创建网站的语言 —— 但标记是什么意思？ 标记语言 [https://techterms.com/definition/markup_language] 是使用标签来定义文本文件中不同元素的语言。大多数人都熟悉的富文本编辑器就是一种允许用户在他们的文件中添加额外的格式、图像和链接的程序。 Microsoft Word（一种富文本编辑器）的用户界面截图。标记语言使用如下格式的表情标签：  * <p> </p> 是一对段落标签。  * <b> </b> 加粗标签内的文字. 标记语言家族有许多成员，比如 XML [https://baike.baidu.com/item/%E5%8F%AF%E6%89%A9%E5%B1%95%E6%A0%87%E8%AE%B0%E8%AF%AD%E8%A8%80/2885849] 、HTML [https://bai ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/markdown-cheatsheet/</link>
                <guid isPermaLink="false">63c8c5fd42d274071ebbef20</guid>
                
                    <category>
                        <![CDATA[ markdown ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Thu, 19 Jan 2023 04:41:23 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/01/Markdown-cheatsheet.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/markdown-cheatsheet/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Markdown Cheat Sheet – How to Write Articles in Markdown Language</a>
      </p><!--kg-card-begin: markdown--><p>作为一名开发者，你很可能听说过 <a href="https://baike.baidu.com/item/HTML/97049">HTML</a>，即超文本标记语言（<strong>H</strong>yper<strong>T</strong>ext <strong>M</strong>arkup <strong>L</strong>anguage）。</p>
<p>你可能还知道 HTML 是一种用于创建网站的语言 —— 但<strong>标记</strong>是什么意思？</p>
<p><a href="https://techterms.com/definition/markup_language">标记语言</a>是使用标签来定义文本文件中不同元素的语言。大多数人都熟悉的<strong>富文本编辑器</strong>就是一种允许用户在他们的文件中添加额外的格式、图像和链接的程序。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
  <img src="https://www.freecodecamp.org/news/content/images/2022/08/image-30.png" class="kg-image" alt="Microsoft Word（一种富文本编辑器）的用户界面截图。" width="600" height="400" loading="lazy">
  <figcaption>Microsoft Word（一种富文本编辑器）的用户界面截图。</figcaption>
</figure>
<p>标记语言使用如下格式的表情标签：</p>
<ul>
<li>&lt;p&gt; &lt;/p&gt; 是一对段落标签。</li>
<li>&lt;b&gt; &lt;/b&gt; 加粗标签内的文字.</li>
</ul>
<p>标记语言家族有许多成员，比如 <a href="https://baike.baidu.com/item/%E5%8F%AF%E6%89%A9%E5%B1%95%E6%A0%87%E8%AE%B0%E8%AF%AD%E8%A8%80/2885849">XML</a>、<a href="https://baike.baidu.com/item/HTML/97049">HTML</a>，以及本文的主题：<strong>Markdown</strong>。</p>
<p>开发人员常用 Markdown 来编写文档 —— 通常在大多数仓库中都有 markdown 格式的文档。例如，这篇文章正是我在 freeCodeCamp 上使用 Markdown 编写的。</p>
<p>那么，让我们看看我们可以用 Markdown 做什么。</p>
<p><strong>免责声明：</strong> 没有一个统一的机构或规范来标准化 Markdown —— 只有一些被广泛接受的最佳实践。因此，你可能需要根据使用的 Markdown 解析器来从下面选择不同的内容。</p>
<h1 id="markdown">Markdown 速查表</h1>
<p>下面是一些最常用的在 Markdown 中操作文本的方法。</p>
<h1 id="markdown">如何在 Markdown 中创建标题</h1>
<p>Markdown 中有六种样式的标题，从 H1 到 H6。接下来，我将给你演示他们看起来分别是什么样的，以及如何用 Markdown 创建它们。</p>
<p>H1 字体最大，也被通常作为“主标题”，接下来的每个标题的字体都依次变小。</p>
<h1 id="h1tag">H1 tag</h1>
<p><code># H1 tag</code></p>
<h2 id="h2tag">H2 tag</h2>
<p><code>## H2 tag</code></p>
<h3 id="h3tag">H3 tag</h3>
<p><code>### H3 tag</code></p>
<h4 id="h4tag">H4 tag</h4>
<p><code>#### H4 tag</code></p>
<h5 id="h5tag">H5 Tag</h5>
<p><code>##### H5 tag</code></p>
<h6 id="h6tag">H6 tag</h6>
<p><code>###### H6 tag</code></p>
<h1 id="markdown">如何在 Markdown 中增加强调排版</h1>
<p>通常，你会将文字加粗、设为斜体，或者添加删除线来强调一段文字。过多的强调组合反而会使文字变得更不清晰，所以请仔细选择你想强调的每一段文字的方式。</p>
<p>Markdown 中还有下标和上标符号，例如，你可以用它们来写化合物的名称，或者是作为数学符号的一部分。</p>
<p><strong>如何把文本加粗：</strong></p>
<p>在文本周围添加两个星号，将使该文本显示为粗体。就像这样：<code>**粗体**</code>。</p>
<p><em>如何显示斜体文本：</em></p>
<p>在文本周围添加单个星号，就可以把它设置为斜体显示。像这样：<code>*斜体s*</code>。</p>
<p>如何在特定文字上添加<s>删除线</s>：</p>
<p>如果你想在文本中“划掉一些东西”，你可以用上删除线，比如：<code>~~划掉~~</code>。</p>
<h3 id="markdown">如何在 Markdown 中写下标</h3>
<p>举个例子，如果你想写水的化学符号，你可以通过输入 <code>H~2~0</code> 来将“2”显示为下标。</p>
<p>渲染后的样式是 H~2~0。</p>
<h3 id="markdown">如何在 Markdown 中写上标</h3>
<p>假设你想写一个指数或上标。你可以像这样做：<code>X^2^</code>，你将得到 X^2^。</p>
<h1 id="markdown">如何用Markdown制作列表</h1>
<p>Markdown 中有多种类型的列表，比如有序列表和无序列表。</p>
<p>有序列表通常用于你想按一定顺序进行的步骤（比如按照菜谱煮鸡，直到上菜）。但是对于那些不需要像菜谱那样有顺序的步骤的事情（例如，购物清单）来说，可以使用无序列表。</p>
<h3 id="markdown">如何在 Markdown 中制作一个无序列表</h3>
<p>这是一个无序列表的样子。</p>
<ul>
<li>辣椒油</li>
<li>米饭</li>
<li>大葱</li>
</ul>
<p>Markdown 代码中，在行首用 <code>- </code> 来创建一个无需列表。</p>
<pre><code>- 辣椒油
- 米饭
- 大葱
</code></pre>
<h3 id="markdown">如何在 Markdown 中制作一个有序列表</h3>
<p>这是一个有序列表的样子。</p>
<ol>
<li>第一项</li>
<li>第二项</li>
</ol>
<p>Markdown 代码中，在行首用 <code>1. </code> 来创建一个有需列表（译者注：数字编号可以是自己维护的，也可以始终为一个数字，比如 1）。</p>
<pre><code>1. 第一项
2. 第二项
</code></pre>
<h1 id="markdown">如何在 Markdown 中插入链接</h1>
<p>在 Markdown 文档中的两种最常见的链接是超链接和图片。这两种方式都能使你的文章更清晰、更有说服力，你应在适当的时候使用。</p>
<p>下面是文本中的超链接的样子：</p>
<p><a href="https://www.kealanparr.com">Kealan 的网站</a></p>
<p>这是创建它的 Markdown 代码：</p>
<p><code>[Kealan 的网站](https://www.kealanparr.com)</code></p>
<p>你需要把想在超链接上显示的文字放在方括号里（这里是 "Kealan 的网站"），紧接着用小括号包裹指向的 URL。</p>
<p>又比如，你想在一篇文章中加入一张图片，像这个样子：</p>
<p><img src="https://images.unsplash.com/photo-1660866838784-6c5158c0f979?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=387&amp;q=80" alt="天然岩石景观形成的山谷，绵延到蓝天下的道路交叉处。" width="600" height="400" loading="lazy"></p>
<p>用以下的记号来写就可以：</p>
<pre><code>![天然岩石景观形成的山谷，绵延到蓝天下的道路交叉处。](https://images.unsplash.com/photo-1660866838784-6c5158c0f979?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=387&amp;q=80)
</code></pre>
<p>这跟普通的超链接相似，只是在方括号前面加上感叹号。</p>
<h2 id="markdownhtml">如何在 Markdown 中使用 HTML 代码</h2>
<p>你可以直接在 Markdown 文档中使用普通的 HTML（这取决于你所使用的解释器）。</p>
<p>所以你可以随意输入你喜欢的任何有效的 HTML。</p>
<h2 id="markdown">如何在 Markdown 中添加分隔符</h2>
<p>如果你想通过一根横线来分隔文本中的章节，你可以获得这样的效果：</p>
<hr>
<p>只需要在一行中键入三个横线：</p>
<pre><code>---
</code></pre>
<h2 id="markdown">如何用 Markdown 制作表格</h2>
<p>表格在你的文章中很方便。要制作一个看起来像这样的表格：</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>Kealan</td>
<td>25</td>
</tr>
<tr>
<td>Jake</td>
<td>28</td>
</tr>
</tbody>
</table>
<p>你需要用到这样的标记：</p>
<pre><code>| Name   | Age |
| ------ | --- |
| Kealan | 25  |
| Jake   | 28  |
</code></pre>
<p>在制作 Markdown 表格时，唯一你必须注意的问题是，你要保持管道符（|）垂直排列。那么你的表格就会像本文上面的那样。这个图片更能清晰地说明问题。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/image-139.png" alt="image-139" width="600" height="400" loading="lazy"></p>
<p>这显示了一个 Markdown 表格，以 Name 和 Age 为标题，包含两行数值，Kealan、Jake 和 25、28。</p>
<h2 id="markdown"><strong>如何在 Markdown 中添加代码及语法高亮</strong></h2>
<p>如果你为开发者创建文档，在你的 Markdown 中添加代码片段会有很大的帮助。</p>
<p>下面是一个非常简单的 JavaScript 例子，但 Markdown 几乎支持所有的现代编程语言（包括对齐进行语法高亮等）。</p>
<pre><code class="language-javascript">console.log('example log')
</code></pre>
<pre><code>```javascript  
console.log('example log')  
```
</code></pre>
<p>只要输入三个反引号，并加上编程语言的名字，再回车就可以开始写代码了。再用三个反引号来结束代码块。</p>
<h1 id="markdown">如何在 Markdown 中添加引用</h1>
<p>当你提及别人的作品时，你应该有礼貌地引用他们的作品。一个简单的方法是引用他们的话。</p>
<p>如果你想在 Markdown 中加入引用：</p>
<blockquote>
<p>“这是一句话，来自一个非常明智的人” —— 佚名</p>
</blockquote>
<p>在行首加上这个符号，就能将其渲染成上面的样子：</p>
<p><code>&gt; “这是一句话，来自一个非常明智的人” —— 佚名</code></p>
<h1 id="">结语</h1>
<p>我希望这篇文章对你来说是一个有用的参考，也希望你能从中学到你以前没见过的 Markdown 新特性。</p>
<p>Markdown 还有很多功能（甚至还没算上你可以创建的 HTML 变体），但本文已经涵盖了最常用的功能。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 用 Azure AI 解决数独问题 ]]>
                </title>
                <description>
                    <![CDATA[ 这篇文章将带你使用 Azure 表单识别器创建一个数独解题器，Azure 表单识别器是一个由 AI 驱动的文档提取服务。 我们的程序将首先让用户上传一张数独表的图片，接下来我们将从图像中提取数据，然后基于此数据实现数独解题算法。 我们将在后端使用 .NET，在前端使用 Angular，并用 Angular material 来设计程序的 UI 风格。 下面是此软件的一个演示。 准备  * 从 https://nodejs.org/zh-cn/download/  下载并安装最新的 LTS 版本的 Node.js  * 从 https://cli.angular.io/  下载并安装 Angular CLI  * 一个 Azure 订阅账号，你可以在 https://azure.microsoft.com/zh-cn/free/  创建一个免费的 Azure 账号  * 从 https://dotnet.microsoft.com/zh-cn/download/dotnet/5.0  下载并安装 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/solve-sudoku-using-azure-ai/</link>
                <guid isPermaLink="false">6300954060480505ded7a836</guid>
                
                    <category>
                        <![CDATA[ Azure ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 数独 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Fri, 19 Aug 2022 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/08/6065634f9618b008528ab1f6.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/solve-sudoku-using-azure-ai/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Solve a Sudoku Puzzle Using Azure AI</a>
      </p><!--kg-card-begin: markdown--><p>这篇文章将带你使用 Azure 表单识别器创建一个数独解题器，Azure 表单识别器是一个由 AI 驱动的文档提取服务。</p>
<p>我们的程序将首先让用户上传一张数独表的图片，接下来我们将从图像中提取数据，然后基于此数据实现数独解题算法。</p>
<p>我们将在后端使用 .NET，在前端使用 Angular，并用 Angular material 来设计程序的 UI 风格。</p>
<p>下面是此软件的一个演示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/SudokuSolver.gif" alt="SudokuSolver" width="600" height="400" loading="lazy"></p>
<h2 id="">准备</h2>
<ul>
<li>从 <a href="https://nodejs.org/zh-cn/download/">https://nodejs.org/zh-cn/download/</a> 下载并安装最新的 LTS 版本的 Node.js</li>
<li>从 <a href="https://cli.angular.io/">https://cli.angular.io/</a> 下载并安装 Angular CLI</li>
<li>一个 Azure 订阅账号，你可以在 <a href="https://azure.microsoft.com/zh-cn/free/">https://azure.microsoft.com/zh-cn/free/</a> 创建一个免费的 Azure 账号</li>
<li>从 <a href="https://dotnet.microsoft.com/zh-cn/download/dotnet/5.0">https://dotnet.microsoft.com/zh-cn/download/dotnet/5.0</a> 下载并安装 .NET Core 5.0 SDK</li>
<li>从 <a href="https://visualstudio.microsoft.com/downloads/">https://visualstudio.microsoft.com/downloads/</a> 下载并安装最新版本的 Visual Studio 2019（译者注：已推出了更新版本的 Visual Studio 2022）</li>
</ul>
<h2 id="">源代码</h2>
<p>你可以在 <a href="https://github.com/AnkitSharma-007/Azure-AI-Sudoku-solver">GitHub</a> 上获取源代码。</p>
<h2 id="azure">什么是 Azure 表单识别器认知服务</h2>
<p>得益于 <a href="https://azure.microsoft.com/zh-cn/services/form-recognizer/">Azure 表单识别器</a>认知服务，我们可以使用机器学习技术构建自动数据处理软件。它能从文档中提取文本、键-值对、选择标记、表格和结构。</p>
<p>在 REST API 或客户端库 SDK 的帮助下，我们可以轻松地调用表单识别器模型。</p>
<p>表单识别器认知服务提供了以下功能：</p>
<ul>
<li><strong>预构建模型</strong>：我们可以使用预先建立的模型，从独特的文件类型中提取数据，比如发票、收据、身份证和名片。</li>
<li><strong>自定义模型</strong>：我们可以使用自定义模型从表单中提取文本、键-值对、选择标记和表格数据。但我们需要使用自己的数据来训练自定义模型，使其适合我们的自定义需求。</li>
<li><strong>布局 API</strong>：它允许我们从文件中提取文本、选择标记和表格结构。</li>
</ul>
<p>在这篇文章中，我们将使用布局 API 从用户上传的数独表图片中提取内容。</p>
<h2 id="azure">如何创建 Azure 表单识别器认知服务资源</h2>
<p>登录 Azure 门户，在搜索栏中搜索“认知服务”并点击结果。在下一个屏幕上，点击“创建”按钮。这将打开认知服务市场的页面。在搜索栏中搜索“表单识别器”，并点击搜索结果中的“表单识别器”选项。</p>
<p>这将打开表单识别器 API 页面。点击“创建”按钮来新建一个表单识别器资源。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/CreateFR.png" alt="CreateFR" width="600" height="400" loading="lazy"></p>
<p>在创建表格识别器的页面中，按照下面的指引填写详细信息。</p>
<ul>
<li><strong>订阅 / Subscription</strong>：从下拉菜单中选择订阅类型。</li>
<li><strong>资源组 / Resource group</strong>：选择一个现有的资源组或新建一个资源组。</li>
<li><strong>地区 / Region</strong>：选择你所在的地区。</li>
<li><strong>名称 / Name</strong>：为你的资源起一个独特的名字。</li>
<li><strong>定价 / Pricing tier</strong>：选择你想要的定价级别。</li>
</ul>
<p>点击“检查并创建”按钮。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/ConfigureFR.png" alt="ConfigureFR" width="600" height="400" loading="lazy"></p>
<p>在下一个页面，勾选确认使用条款，检查核对你所提供的信息，然后点击“创建”按钮。</p>
<p>在你的资源被成功部署后，点击“转到资源”按钮。点击左边菜单上的“密钥和端点”链接。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/FRKeys.png" alt="FRKeys" width="600" height="400" loading="lazy"></p>
<p>记下端点和页面上提供的任意一个密钥。我们将在本文的后半部分使用这些来从 .NET 代码中调用表单识别器服务的布局 API。</p>
<h2 id="aspnetcore">如何创建 ASP.NET Core 程序</h2>
<p>打开 Visual Studio 2019（或更新版本），点击“创建一个新项目”，这将打开一个“创建新项目”对话框。选择“ASP.NET Core with Angular”并点击“下一步”。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/CreateProj.png" alt="CreateProj" width="600" height="400" loading="lazy"></p>
<p>现在你将进入“配置新项目”界面。为该应用程序设置一个名称，比如 <code>ngSudokuSolver</code>，并点击“下一步”。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/CreateProj_1.png" alt="CreateProj_1" width="600" height="400" loading="lazy"></p>
<p>在附加信息页面，选择目标框架为 .NET 5.0，并将认证类型设置为“无”，如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/CreateProj_2.png" alt="CreateProj_2" width="600" height="400" loading="lazy"></p>
<p>现在，我们创建了一个工程。这个程序的文件夹结构应与下图类似：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/Sol_Exp.png" alt="Sol_Exp" width="600" height="400" loading="lazy"></p>
<p><code>ClientApp</code> 文件夹中包含我们程序的 Angular 代码。<code>Controllers</code> 文件夹将包含 API 控制器。Angular 组件则位于 <code>ClientApp/src/app</code> 文件夹中。</p>
<p>默认模板包含了一些 Angular 组件。这些组件不会影响我们的应用程序，但为了简单起见，我们将从 <code>ClientApp/src/app</code> 文件夹中删除 <code>fetchdata</code> 和 <code>counter</code> 文件夹。同时，从 <code>app.module.ts</code> 文件中删除对这两个组件的引用。</p>
<h2 id="nuget">如何安装所需的 NuGet 包</h2>
<p>要安装 NuGet 包，请打开 工具 &gt;&gt; NuGet 包管理器 &gt;&gt; 包管理器控制台。这将在 Visual Studio 内打开包管理器控制台。</p>
<p>运行以下命令来安装 <a href="https://www.nuget.org/packages/Polly">Polly</a>。这个库能让你以流畅和线程安全的方式表达弹性和瞬时故障处理策略，如重试、断路、超时、隔板隔离和回退。</p>
<p><code>Install-Package Polly -Version 7.2.1</code></p>
<p>运行以下命令来安装 <a href="https://www.nuget.org/packages/Newtonsoft.Json/">Newtonsoft.Json</a>。</p>
<p><code>Install-Package Newtonsoft.Json -Version 13.0.1</code></p>
<h2 id="retrymessagehandler">如何创建 RetryMessage Handler</h2>
<p>右键单击 <code>ngSudokuSolver</code> 项目，选择 添加 &gt;&gt; 新文件夹。将该文件夹命名为 Models。</p>
<p>接下来，右键单击 Models 文件夹并选择 添加 &gt;&gt; 类 来添加一个新的类文件。把这个类的名字定为 <code>HttpRetryMessageHandler.cs</code>，然后点击“添加”。</p>
<p>将下面的代码放入这个类里面。</p>
<pre><code class="language-C#">using Newtonsoft.Json;
using Polly;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace ngSudokuSolver.Models
{
    public class HttpRetryMessageHandler : DelegatingHandler
    {
        public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) { }

        protected override Task&lt;HttpResponseMessage&gt; SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken) =&gt;
            Policy
                .Handle&lt;HttpRequestException&gt;()
                .Or&lt;TaskCanceledException&gt;()
                .OrResult&lt;HttpResponseMessage&gt;(x =&gt;
                {
                    string result = x.Content.ReadAsStringAsync().GetAwaiter().GetResult();
                    dynamic array = JsonConvert.DeserializeObject(result);

                    if (array["status"] == "running")
                    {
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                })
                .WaitAndRetryAsync(7, retryAttempt =&gt; TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
                .ExecuteAsync(() =&gt; base.SendAsync(request, cancellationToken));
    }
}
</code></pre>
<p>我们将使用 RetryMessageHandler 来重试对 <code>sendAsync</code> 的调用。如果 HttpResponseMessage 的状态是“运行中”，我们将重试 HTTP 调用。</p>
<p>我们把最大重试次数设置为 7 次。每次重试时，都会增加等待时间，将其翻倍。如果所有重试次数已经用完，而 HttpResponseMessage 还没有成功，我们将返回 false。</p>
<h2 id="formrecognizer">如何加入 FormRecognizer 控制器</h2>
<p>我们现在在程序里加入一个新的控制器。</p>
<p>右键单击 Controllers 文件夹，选择 添加 &gt;&gt; 新项目。这将打开一个“添加新项目”的对话框。</p>
<p>从左侧栏选择 “Visual C#”，然后在模版栏中选择 “API Controller-Empty”，并将其命名为 <code>FormRecognizerController.cs</code>，点击“添加”。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/AddController.png" alt="AddController" width="600" height="400" loading="lazy"></p>
<p>将以下代码放入该类。</p>
<pre><code class="language-C#">using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ngSudokuSolver.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace ngSudokuSolver.Controllers
{
    [Produces("application/json")]
    [Route("api/[controller]")]
    public class FormRecognizerController : ControllerBase
    {
        static string endpoint;
        static string apiKey;

        public FormRecognizerController()
        {
            endpoint = "https://sudokusolver.cognitiveservices.azure.com/";
            apiKey = "a9f75796b3ba49bdade48eb3b905cb0e";
        }

        [HttpPost, DisableRequestSizeLimit]
        public async Task&lt;string[][]&gt; Post()
        {
            try
            {
                string[][] sudokuArray = GetNewSudokuArray();

                if (Request.Form.Files.Count &gt; 0)
                {
                    var file = Request.Form.Files[Request.Form.Files.Count - 1];

                    if (file.Length &gt; 0)
                    {
                        var memoryStream = new MemoryStream();
                        file.CopyTo(memoryStream);
                        byte[] imageFileBytes = memoryStream.ToArray();
                        memoryStream.Flush();

                        string SudokuLayoutJSON = await GetSudokuBoardLayout(imageFileBytes);
                        if (SudokuLayoutJSON.Length &gt; 0)
                        {
                            sudokuArray = GetSudokuBoardItems(SudokuLayoutJSON);
                        }
                    }
                }

                return sudokuArray;
            }
            catch
            {
                throw;
            }
        }

        static async Task&lt;string&gt; GetSudokuBoardLayout(byte[] byteData)
        {
            HttpClient client = new();
            client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiKey);
            string uri = endpoint + "formrecognizer/v2.1-preview.3/layout/analyze";
            string LayoutJSON = string.Empty;

            using (ByteArrayContent content = new(byteData))
            {
                HttpResponseMessage response;
                content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
                response = await client.PostAsync(uri, content);

                if (response.IsSuccessStatusCode)
                {
                    HttpHeaders headers = response.Headers;

                    if (headers.TryGetValues("Operation-Location", out IEnumerable&lt;string&gt; values))
                    {
                        string OperationLocation = values.First();
                        LayoutJSON = await GetJSON(OperationLocation);
                    }
                }
            }
            return LayoutJSON;
        }

        static async Task&lt;string&gt; GetJSON(string endpoint)
        {
            using var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler()));
            var request = new HttpRequestMessage();
            request.Method = HttpMethod.Get;
            request.RequestUri = new Uri(endpoint);

            client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiKey);

            var response = await client.SendAsync(request);
            var result = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();

            return result;
        }

        static string[][] GetSudokuBoardItems(string LayoutData)
        {
            string[][] sudokuArray = GetNewSudokuArray();
            dynamic array = JsonConvert.DeserializeObject(LayoutData);
            int countOfCells = ((JArray)array?.analyzeResult?.pageResults[0]?.tables[0]?.cells).Count;

            for (int i = 0; i &lt; countOfCells; i++)
            {
                int rowIndex = array.analyzeResult.pageResults[0].tables[0].cells[i].rowIndex;
                int columnIndex = array.analyzeResult.pageResults[0].tables[0].cells[i].columnIndex;

                sudokuArray[rowIndex][columnIndex] = array.analyzeResult.pageResults[0].tables[0].cells[i]?.text;
            }
            return sudokuArray;
        }

        static string[][] GetNewSudokuArray()
        {
            string[][] sudokuArray = new string[9][];

            for (int i = 0; i &lt; 9; i++)
            {
                sudokuArray[i] = new string[9];
            }

            return sudokuArray;
        }
    }
}
</code></pre>
<p>在 FormRecognizerController 类的构造函数中，我们初始化了表单识别器 API 的密钥和端点 URL。</p>
<p>上面的 Post 方法将接收请求体中作为文件集合的图像数据，并返回一个二维数组。我们将把图像数据转换为字节数组并调用 <code>GetSudokuBoardLayout</code> 方法。如果我们得到一个成功的响应并且 JSON 结果非空，我们将调用 <code>GetSudokuBoardItems</code> 方法。</p>
<p>在 <code>GetSudokuBoardLayout</code> 方法中，我们将实例化一个新的 HttpClient。我们会在请求头中传递订阅密钥。</p>
<p>当我们调用表单识别器 API 时，Azure 服务将返回状态码 202。这表明该服务已经接受了请求，并将在稍后开始处理。</p>
<p>响应包括一个“Operation-Location”首部。“Operation-Location”字段里的数据就我们将用来获取表单识别结果的 URL，这个 URL 会在 48 小时后过期。</p>
<p>在获取表单识别结果前需要等待一段时间，等待时间的长短与文本的长度相关。</p>
<p>这里就要用到了我们前面配置的 RetryMessageHandler。我们将从“Operation-Location”首部获取 URL，并调用 <code>GetJSON</code> 方法来获取 JSON 结果。</p>
<p>在 <code>GetJSON</code> 方法中，我们创建了一个 HttpClient，并用自定义的 <code>HttpRetryMessageHandler</code> 来初始化它。这个方法将以字符串的形式返回 JSON 响应。</p>
<p><code>GetSudokuBoardItems</code> 方法接受 JSON 字符串。然后它将遍历 JSON 字符串中的 table 属性，以准备二维的 <code>sudokuArray</code>。</p>
<h2 id="">现在，让我们开始实现程序的客户端部分</h2>
<p>客户端的代码位于 ClientApp 文件夹中。我们将使用 Angular CLI 来处理客户端的代码。</p>
<blockquote>
<p>Angular CLI 并非你的唯一选择。本文中使用 Angular CLI 是因为它对用户友好且直接。如果你不想使用 CLI，你可以手动创建组件和服务的文件。</p>
</blockquote>
<p>进入 <code>ngSudokuSolver/ClientApp</code> 文件夹并在此打开一个命令窗口。我们将在这个窗口中执行我们所有的 Angular CLI 命令。</p>
<h2 id="angularmaterial">如何安装 Angular Material</h2>
<p>运行以下命令以将 Angular Material 添加到项目中。</p>
<p><code>ng add @angular/material</code></p>
<p>该命令将为你的项目安装 Angular Material，它会询问以下问题，以确定要包括哪些功能：</p>
<ul>
<li>Choose a prebuilt theme name, or "custom" for a custom theme: <strong>请选择 Indigo/Pink 主题</strong></li>
<li>Set up global Angular Material typography styles? (Y/n): <strong>Y</strong></li>
<li>Set up browser animations for Angular Material? (Y/n): <strong>Y</strong></li>
</ul>
<p>如下图所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/ngMaterial.png" alt="ngMaterial" width="600" height="400" loading="lazy"></p>
<h2 id="angularmaterial">如何为 Angular Material 添加模块</h2>
<p>运行以下命令来创建一个新的模块。</p>
<p><code>ng g m ng-material</code></p>
<p>将下面的代码放入 <code>src\app\ng-material\ng-material.module.ts</code> 文件中。</p>
<pre><code class="language-typescript">import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';

const materialModules = [
  MatButtonModule,
  MatCardModule,
  MatInputModule,
  MatToolbarModule,
  MatDividerModule,
  MatIconModule,
];

@NgModule({
  declarations: [],
  imports: [CommonModule, ...materialModules],
  exports: [...materialModules],
})
export class NgMaterialModule {}
</code></pre>
<p>我们正在导入我们将在这个应用程序中使用的 Angular material 组件的所有必要模块。独立的 Angular material 模块将使该应用易于维护。</p>
<p>在 <code>app.module.ts</code> 文件中导入 <code>NgMaterialModule</code>，如下所示：</p>
<pre><code class="language-typescript">import { NgMaterialModule } from './ng-material/ng-material.module';

@NgModule({
	...
	imports: [
		...
		NgMaterialModule,
	],
})
</code></pre>
<h2 id="">如何配置程序的导航栏</h2>
<p>打开 <code>nav-menu.component.html</code>，在里面放入以下代码。</p>
<pre><code class="language-html">&lt;mat-toolbar color="primary" class="mat-elevation-z2"&gt;
  &lt;mat-toolbar-row&gt;
    &lt;div&gt;
      &lt;button mat-button [routerLink]='["/"]'&gt;
        &lt;mat-icon&gt;book&lt;/mat-icon&gt; Sudoku Solver
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/mat-toolbar-row&gt;
&lt;/mat-toolbar&gt;
</code></pre>
<p>现在，我们成功地在程序中加入了 Angular material 工具栏，和一个链接到程序根路由的按钮。</p>
<h2 id="">如何创建表单识别器服务</h2>
<p>我们将创建一个 Angular 服务，它将调用 Web API 端点并将响应传递给我们的组件。请运行下面的命令。</p>
<p><code>ng g s services\form-recognizer</code></p>
<p>这个命令将创建一个名为 services 的文件夹，然后在里面创建以下两个文件：</p>
<ul>
<li>form-recognizer.service.ts - 服务类文件</li>
<li>form-recognizer.service.spec.ts - 服务的单元测试文件</li>
</ul>
<p>将下面的代码放入 <code>form-recognizer.service.ts</code> 文件中。</p>
<pre><code class="language-typescript">import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root',
})
export class FormRecognizerService {
  baseURL: string;

  constructor(private http: HttpClient) {
    this.baseURL = '/api/FormRecognizer';
  }

  getSudokuTableFromImage(image: FormData) {
    return this.http.post(this.baseURL, image);
  }
}
</code></pre>
<p>我们定义了一个存有 API 的端点 URL 的变量 baseURL。我们会在构造器中初始化 baseURL，并将其赋值为 <code>FormRecognizerController</code> 的端点。</p>
<p><code>getSudokuTableFromImage</code> 方法将向 <code>FormRecognizerController</code> 发送一个 Post 请求并提供 FormData 类型的参数。它将获取一个表示数独表的二维数组。</p>
<h2 id="">如何更新主页组件</h2>
<p>把下面的代码放入 <code>home.component.html</code> 中。</p>
<pre><code class="language-html">&lt;div class="container"&gt;
  &lt;h1 class="display-4"&gt; 用 Azure AI 解决数独问题 &lt;/h1&gt;
  &lt;mat-divider&gt;&lt;/mat-divider&gt;
  &lt;div class="row mt-3"&gt;
    &lt;div class="col-md-6"&gt;
      &lt;mat-card class="mat-elevation-z4"&gt;
        &lt;mat-card-content&gt;
          &lt;table&gt;
            &lt;tr *ngFor="let row of gameBoard"&gt;
              &lt;td *ngFor="let col of gameBoard"&gt;
                {{game[row][col]}}
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/table&gt;
        &lt;/mat-card-content&gt;
        &lt;mat-card-actions&gt;
          &lt;button type="button" mat-raised-button color="primary" (click)="SolveSudoku()"&gt; 解数独 &lt;/button&gt;
        &lt;/mat-card-actions&gt;
      &lt;/mat-card&gt;
    &lt;/div&gt;
    &lt;div class="col-md-6"&gt;
      &lt;div class="image-container"&gt;
        &lt;img class="preview-image" src={{imagePreview}}&gt;
      &lt;/div&gt;
      &lt;input type="file" (change)="uploadImage($event)" /&gt;
      &lt;hr /&gt;
      &lt;button mat-raised-button color="accent" (click)="GetSudokuTable()"&gt;
        &lt;span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"&gt;&lt;/span&gt; 提取数独表
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>我们创建了一个 9x9 的数独表格。我们定义了一个支持上传图片的文件上传控件。上传图片后，<code>&lt;img&gt;</code> 元素中将显示图片的预览。</p>
<p>点击“提取数独表”按钮将从图像中获取数独的内容，并将这些数字填入表格中。点击“解数独”将解决数独，并将结果更新到表格中。</p>
<p>把以下代码放入 <code>home.component.ts</code> 中。</p>
<pre><code class="language-typescript">import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { FormRecognizerService } from '../services/form-recognizer.service';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnDestroy {
  gameBoard = [0, 1, 2, 3, 4, 5, 6, 7, 8];
  loading = false;
  imageFile;
  imagePreview;
  maxFileSize: number;
  isValidFile = true;
  status: string;
  DefaultStatus: string;
  imageData = new FormData();
  game = new Array(9);
  private unsubscribe$ = new Subject();

  constructor(private formRecognizerService: FormRecognizerService) {
    this.DefaultStatus = '允许上传的最大图像大小为 4 MB';
    this.status = this.DefaultStatus;
    this.maxFileSize = 4 * 1024 * 1024; // 4MB

    for (var i = 0; i &lt; this.game.length; i++) {
      this.game[i] = new Array(9);
    }
  }

  uploadImage(event) {
    this.imageFile = event.target.files[0];
    if (this.imageFile.size &gt; this.maxFileSize) {
      this.status = `文件大小为 ${this.imageFile.size} 比特，超出所允许的大小上限 ${this.maxFileSize} 比特。`;
      this.isValidFile = false;
    } else if (this.imageFile.type.indexOf('image') == -1) {
      this.status = '请上传有效的图片文件';
      this.isValidFile = false;
    } else {
      const reader = new FileReader();
      reader.readAsDataURL(event.target.files[0]);
      reader.onload = () =&gt; {
        this.imagePreview = reader.result;
      };
      this.status = this.DefaultStatus;
      this.isValidFile = true;
    }
  }

  GetSudokuTable() {
    if (this.isValidFile) {
      this.loading = true;
      this.imageData.append('imageFile', this.imageFile);

      this.formRecognizerService
        .getSudokuTableFromImage(this.imageData)
        .pipe(takeUntil(this.unsubscribe$))
        .subscribe(
          (result: any) =&gt; {
            this.game = result;
            this.loading = false;
          },
          () =&gt; {
            console.error();
            this.loading = false;
          }
        );
    }
  }

  SolveSudoku() {
    this.sudokuSolver(this.game);
  }

  ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }

  private sudokuSolver(data) {
    for (let i = 0; i &lt; 9; i++) {
      for (let j = 0; j &lt; 9; j++) {
        if (data[i][j] == '') {
          for (let k = 1; k &lt;= 9; k++) {
            if (this.isSudokuValid(data, i, j, k)) {
              data[i][j] = `${k}`;
              if (this.sudokuSolver(data)) {
                return true;
              } else {
                data[i][j] = '';
              }
            }
          }
          return false;
        }
      }
    }
    return true;
  }

  private isSudokuValid(board, row, col, k) {
    for (let i = 0; i &lt; 9; i++) {
      const m = 3 * Math.floor(row / 3) + Math.floor(i / 3);
      const n = 3 * Math.floor(col / 3) + (i % 3);
      if (board[row][i] == k || board[i][col] == k || board[m][n] == k) {
        return false;
      }
    }
    return true;
  }
}
</code></pre>
<p>我们在 <code>HomeComponent</code> 的构造器中注入 formRecognizerService，并设置提示信息和允许的图像的最大尺寸。我们还将初始化一个二维数组来保存数独的值。</p>
<p>上传图片时将调用 <code>uploadImage</code> 方法。我们将检查上传的文件是否是一个有效的图像，并且在允许的大小限制之内。我们将使用一个 FileReader 对象来处理图像数据。readAsDataURL 方法将读取上传文件的内容。</p>
<p>当读取操作成功完成后，reader.onload 事件将会被触发。imagePreview 的值将被设置为 fileReader 对象的 ArrayBuffer 类型的返回值。</p>
<p>在 <code>GetSudokuTable</code> 方法中，我们将把图像文件附加到一个 FormData 类型的变量中。我们将调用服务的 <code>getSudokuTableFromImage</code>，并将结果绑定到游戏数组中。</p>
<p><code>sudokuSolver</code> 方法将接受数独表作为入参。然后我们使用回溯算法来解出这道数独。</p>
<h2 id="">运行演示程序</h2>
<p>按 F5 键来启动该程序。上传一张数独表的图片。点击“提取数独表”按钮，你会看到左边的表格中出现了从图像中提取出来的内容。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/ExecDemo1.png" alt="ExecDemo1" width="600" height="400" loading="lazy"></p>
<p>点击“解数独”按钮。你可以在界面上看到数独的答案。如下图所示。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/ExecDemo2.png" alt="ExecDemo2" width="600" height="400" loading="lazy"></p>
<h2 id=""><strong>总结</strong></h2>
<p>我们用 Angular 和 Azure 表单识别器服务创建了一个数独解题器。这个程序可以从用户上传的数独表的图片中提取数据。我们接着用回溯法解决数独问题。另外，我们使用了 Angular material 来设计程序的 UI 风格。</p>
<p>你可以从 <a href="https://github.com/AnkitSharma-007/Azure-AI-Sudoku-solver">GitHub</a> 上获取源代码，随便看看或把玩一下以加深你的理解。</p>
<h2 id="">拓展阅读</h2>
<ul>
<li><a href="https://ankitsharmablogs.com/optical-character-reader-using-angular-and-azure-computer-vision/">Optical Character Reader Using Angular And Azure Computer Vision（使用 Angular 和 Azure 计算机视觉的光学字符阅读器）</a></li>
<li><a href="https://ankitsharmablogs.com/multi-language-translator-using-blazor-and-azure-cognitive-services/">Multi-Language Translator Using Blazor And Azure Cognitive Services（使用 Blazor 和 Azure 认知服务的多语言翻译器）</a></li>
<li><a href="https://ankitsharmablogs.com/facebook-authentication-and-authorization-in-server-side-blazor-app/">Facebook Authentication And Authorization In Server-Side Blazor App（在服务器端 Blazor 应用中进行 Facebook 认证和授权）</a></li>
<li><a href="https://ankitsharmablogs.com/continuous-deployment-for-angular-app-using-heroku-and-github/">Continuous Deployment For Angular App Using Heroku And GitHub（使用 Heroku 和 GitHub 为 Angular 应用程序进行持续部署）</a></li>
<li><a href="https://ankitsharmablogs.com/going-serverless-with-blazor/">Going Serverless With Blazor（利用 Blazor 实现无服务器化）</a></li>
</ul>
<p>如果你喜欢这篇文章，请把它分享给你的朋友。你也可以在<a href="https://twitter.com/ankitsharma_007">推特</a>和<a href="https://www.linkedin.com/in/ankitsharma-007/">领英</a>上与我联系。</p>
<p>原文发表于 <a href="https://ankitsharmablogs.com/">https://ankitsharmablogs.com/</a>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 软件工程师写作技巧——如何成为一个更好的技术写作者 ]]>
                </title>
                <description>
                    <![CDATA[ 你或许以为软件开发全是围绕写代码进行的，但事实并非如此。这项工作中很大一部分内容是与他人沟通。随着越来越多的工作转向远程办公的形式，书面交流变得越来越重要 [https://stackoverflow.blog/2021/08/09/how-writing-can-advance-your-career-as-a-developer/] 。 > “在一个工程师开始工作的最初几年里，他大概会花 30% 的时间在写作上，而中层管理工程师每天花费 50% 至 70% 的时间写作；高级管理人员花在写作上的时间占到了每天的 70% 以上，甚至 95%。” —— Jon Leydens 我在去年离开了我作为软件工程经理和首席技术官的职位，成为了一名全职写作者。在软件行业工作了十年后，我拿着银行里六个月的存款，决定冒险改变我的职业。 很高兴地说，新职业的一切到现在为止都非常顺利，我的公司最近也因我们的技术文书工作而被 TechCrunch 报道 [https://techcrunch.com/2021/07/29/draft-dev-ceo-karl-hughes-on-the-import ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/writing-tips-software-developers/</link>
                <guid isPermaLink="false">62f360618d13aa0845c6428c</guid>
                
                    <category>
                        <![CDATA[ 科技写作 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ HeZean ]]>
                </dc:creator>
                <pubDate>Wed, 10 Aug 2022 07:45:48 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/08/604adcf9a7946308b7687147.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/writing-tips-software-developers/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Writing Tips for Software Developers – How to Become a Better Tech Writer</a>
      </p><!--kg-card-begin: markdown--><p>你或许以为软件开发全是围绕写代码进行的，但事实并非如此。这项工作中很大一部分内容是与他人沟通。随着越来越多的工作转向远程办公的形式，<a href="https://stackoverflow.blog/2021/08/09/how-writing-can-advance-your-career-as-a-developer/">书面交流变得越来越重要</a>。</p>
<blockquote>
<p>“在一个工程师开始工作的最初几年里，他大概会花 30% 的时间在写作上，而中层管理工程师每天花费 50% 至 70% 的时间写作；高级管理人员花在写作上的时间占到了每天的 70% 以上，甚至 95%。” —— Jon Leydens</p>
</blockquote>
<p>我在去年离开了我作为软件工程经理和首席技术官的职位，成为了一名全职写作者。在软件行业工作了十年后，我拿着银行里六个月的存款，决定冒险改变我的职业。</p>
<p>很高兴地说，新职业的一切到现在为止都非常顺利，我的公司最近也<a href="https://techcrunch.com/2021/07/29/draft-dev-ceo-karl-hughes-on-the-importance-of-using-experts-in-developer-marketing/">因我们的技术文书工作而被 TechCrunch 报道</a>。</p>
<p>在创办 Draft.dev 之前，我已经在外面写了很多年的博客和教程，所以我对自己的写作能力相当自信。但在公司创立后，我依然学到了很多。我也遇到了许多优秀的导师和同行，他们一路走来给我提供了写作方面的建议。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/08/brad-neathery-XrSzacdYbtQ-unsplash.jpg" alt="图片由 Brad Neathery 发布在 Unsplash" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>图片由 Brad Neathery 发布在 Unsplash</figcaption>
</figure>
<p>在这篇文章中，我想分享一些我经常与其他软件开发者分享的写作技巧。他们将帮助你克服所有新写作者会面临的<a href="https://en.wikipedia.org/wiki/Resistance_(creativity)"><em>阻力</em></a>，希望能给你带来尽早开始写作的信心。</p>
<h2 id="1">1. 从你知道的东西开始写起</h2>
<blockquote>
<p>“没有人生来就是伟大的作家。从你目前所知道的东西开始写起，并与社区分享它们。你会惊讶于你能影响多少人的生活。” —— Eze Sunday（软件开发人员、技术作家）</p>
</blockquote>
<p>要想成为一个比现在更好的写作者，你必须比以往更频繁地写作。任何技能都是如此，但对于写作来说，这可能更难，因为你不能只是在一张纸上随意堆砌一些词藻。你必须有写作的 <em>主题</em>。</p>
<p>克服这一障碍的最常见的建议是，<strong>开始写你已经知道的事情</strong>。</p>
<p>在 <a href="https://strapi.io/">Strapi</a> 从事开发者关系的 Daniel Phiri 告诉我：“从你刚解决的问题开始，不管你认为它有多微不足道。”</p>
<p>他也指出，即使一个话题已经被广泛地写过了，你的作品也可以与众不同。“写作是关于视角的，我们每个人的视角就像我们个体本身一样独特。”</p>
<p>Eze Sunday 重申了这个观点：“世上的确有很多文章，但是，在你刚开始的时候，你会发现没有多少好文章会以你喜欢的方式来解释事情。”</p>
<h2 id="2">2. 专注于少数高质量的作品</h2>
<p>“质量胜过数量”，James Hickey 如是跟我说。“专注于写高质量的文章，而不是写一堆 <em>一般</em> 的文章……如果你的内容质量只是 <em>一般</em>，没有人会对它留下深刻印象。”</p>
<p>James 是一位资深的 .NET 开发人员、微软 MVP、作家和演讲者，家里有八个小孩，所以对他而言，找出时间写作总是一个挑战。他的解决办法是谨慎选择他要写的内容，一旦他定好主题，他就会深入研究。</p>
<p>这种做事风格在他的作品中可见一斑，比如这篇登上了 Hacker News 头版的<a href="https://resources.fabric.inc/blog/ecommerce-data-model">关于电子商务数据模型的文章</a>。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/08/More-complex-orders-data-model.png" alt="图表来自 James Hickey 关于电子商务数据模型的文章" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>图表来自 James Hickey 关于电子商务数据模型的文章</figcaption>
</figure>
<p>我同样发现，我最受欢迎的一些博文正是那些真正深入到某个主题的文章。</p>
<p>举个例子，我个人博客上最受欢迎的文章之一是这份 <a href="https://www.karllhughes.com/posts/api-development">4500 字的 API 开发指南</a>。我承认我写了许多比这短的文章，但要把一件事讲透彻，有些话总是要说的。</p>
<h2 id="3">3. “完美”是“足够好”的敌人</h2>
<p>从另一方面来讲，不要让推动你写出最好内容的动力阻止你按下“发布”键。</p>
<p>在 FusionAuth 负责<a href="https://letterstoanewdeveloper.com/">给新开发者的信</a>和开发者关系的 Dan Moore 提出了这个建议：</p>
<blockquote>
<p>“‘完美’是‘足够好’的敌人。为了解决这个问题，我喜欢为自己的写作限时，即使当时间限制到了的时候文章还不完美，我也会发布它……或许你的文章不能登上 Hacker News 的头版，但我能保证，如果你不发表，没有人会读它。”</p>
</blockquote>
<p>许多新手作家过份地在意文章的细节，而不是他们想法的结构与组织。说实话，如果读者能跟上你的逻辑，他们很有可能并不会在意那些拼写和语法错误。</p>
<p>Alpha Particle 公司的首席技术官 Keanan Koppenhaver 告诉我，过度在意完美的语法，可能会使你的作品看上去太过于机械化而毁掉它：</p>
<blockquote>
<p>“我们常常容易陷入试图使你的作品达到最好的状态：完美的语法、伟大的句子结构等等。我曾使用<a href="https://hemingwayapp.com/">海明威编辑器</a>等工具来使我的写作‘技术上正确’，但当我重新阅读我的作品时，它看起来很陈旧，就像由人工智能创作的一样。”</p>
</blockquote>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/08/rock-n-roll-monkey-R4WCbazrD1g-unsplash.jpg" alt="图片由 Rock'n Roll Monkey 发布在 Unsplash" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>图片由 Rock'n Roll Monkey 发布在 Unsplash</figcaption>
</figure>
<h2 id="4">4. 留出时间定期写作</h2>
<blockquote>
<p>“你要像对待任何习惯一样对待[写作]，并给它留出时间。我发现有件事很有帮助，那就是在早上做的第一件事就是写作，你甚至可以早一点起床。我不是一个习惯早起的人，但我仍然觉得这是我最有精力去写作的时候。在这个时刻，没有其它事情会占用我的精神。” —— Adam DuVander（EveryDeveloper 的创始人）</p>
</blockquote>
<p>虽然前面说过，但我要再次重申这一点：<strong>要想成为一名更好的作家，你必须更经常地写作</strong>。不过，这对每个人来说都是不同的。</p>
<p>就我个人而言，<a href="https://draft.dev/learn/technical-content">我每周都会在日历上定好写作时间</a>。我发现，当我专注于写作 4-8 个小时时，我会达到最佳的工作效果，而不是在试图把写作塞进一天中零碎的休息时间。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/08/y1V3iiX.png" alt="y1V3iiX" width="600" height="400" loading="lazy"></p>
<p>当然，并不是所有人都像我这样。<a href="https://www.stephaniemorillo.co/">Stephanie Morillo</a> 是一位技术交流专家，她就适合在更短的时间段里写作。</p>
<blockquote>
<p>“我用时间块（timeboxes）来管理写作：如果我计划写一份讲稿或一篇博文，我会在一天或几天内留出几个 30 分钟的时间段，坐下来写。”</p>
</blockquote>
<p>她指出，这些较小的时间块对她的日程安排来说更现实，它使她能够取得进展并增加产出：“无论你在一天内写 10 个字、100 个字还是 1000 个字，你仍然在朝着你的目标取得进展。”</p>
<p>另一种策略是让写作成为一种日常习惯。经营 Developer Avocados 通讯的 Alex Lakatos 在去年的部分时间里完成了一个每日写作挑战：</p>
<blockquote>
<p>💡 平均下来，形成一个新的习惯需要 66 天。现在离 2021年还剩 65 天，我们何不早点开始进行我们的新年计划呢？</p>
<p>以我的计划为例：我正在努力持续地发布内容，所以在今年剩下的时间里，我将尝试每天至少写 100 字。 <a href="https://t.co/M0dHrJ36ef">pic.twitter.com/M0dHrJ36ef</a></p>
<p>— Alex Lakatos 👨‍💻🥑 (@lakatos88) <a href="https://twitter.com/lakatos88/status/1321423080095469568?ref_src=twsrc%5Etfw">2020 年 10 月 28 日</a></p>
</blockquote>
<p>关键在于，每个人都是不同的，没有一个足够普适的方法来规划预留给写作的时间。Keanan Koppenhaver 跟我说：“最主要的事情是要找到一个时间，让你的大脑能够集中精力、有创造力，真正把你的想法以连贯的方式记录下来。”</p>
<h2 id="5">5. 管理你的期望值</h2>
<blockquote>
<p>“诚然，让别人读你的文章是很好的，但为自己而写作也是非常有价值的，而且你能保证绝对会有人读它。因此，首先也是最重要的事就是为自己写作。” —— Dan Moore</p>
</blockquote>
<p>看到自己写的文章如病毒般传播所带来的快感是非常难以自拔的。在过去十年中，我发表了数百篇博客文章，但只有其中<a href="https://hackernoon.com/how-i-hit-the-front-page-of-hacker-news-5-times-x81n3uyp">五篇登上了 Hacker News 的头版</a>。这并不算什么令人印象深刻的点击率。</p>
<p>这就是为什么你应该主要是为自己而写作。你甚至不需要像 Stephanie Morillo 所说的那样公开发表东西：</p>
<blockquote>
<p>“坚持写日记，写下关于工作、你的一天、你的生活、你的情绪的笔记。写日记让你有机会在没有自我意识的情况下写作，因为你在写作时不需要考虑听众；你只是为自己而写。”</p>
</blockquote>
<p>最后，当你开始写作时，必须牢记你的目标。你是否只是为了记录自己的学习成果而写作？你是想推广一本书、课程或产品吗？你要通过写作获得报酬，还是只是为了好玩？</p>
<p>Adam DuVander 指出，对自己的这些期望保持诚实是至关重要的。他告诉我：“决定写作是作为副业还是主业，这两种选择都是可以的，但你应该设好你的期望值……在工程职位上有许多地方要用到写作。”</p>
<h2 id="">结论</h2>
<blockquote>
<p>你的前100篇博客、视频、帖子、推特、生活、播客、创作可能都是垃圾</p>
<p>克服心里的障碍，去做吧！先过了这 100 关再说</p>
<p>反正几乎没有人会看到它，就把这当作是练习吧</p>
<p>这就是你为创作而付出的代价</p>
<p>— The BKH 🤳🏾 (@thebkh) <a href="https://twitter.com/thebkh/status/1337781548918190082?ref_src=twsrc%5Etfw">2020 年 12 月 12 日</a></p>
</blockquote>
<p>正如 Brian Kofi Hollingsworth 在上文所说的，只有当你开始做一件事，你才可能变得更好。无论你是想利用写作来使你的事业更进一步、赚取副业收入、还是帮助社会上的其他人，如果你想变得更好，你就必须开始多做。</p>
<p>你对那些希望成为更好的作家的软件开发者有什么好建议？如果你有什么要补充的，我很乐意<a href="https://twitter.com/KarlLHughes">在推特上听到你的声音</a>！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
