<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://0end1.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://0end1.github.io/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2026-09-22T09:02:17+08:00</updated><id>https://0end1.github.io/feed.xml</id><title type="html">DevLog</title><subtitle>一个程序员风格的个人博客，记录技术、代码与生活。</subtitle><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><entry><title type="html">TypeScript 学习笔记（2/16）：tsc、类型擦除与严格性旋钮</title><link href="https://0end1.github.io/2026/09/22/typescript-tsc-erased-types-strictness/" rel="alternate" type="text/html" title="TypeScript 学习笔记（2/16）：tsc、类型擦除与严格性旋钮" /><published>2026-09-22T09:00:00+08:00</published><updated>2026-09-22T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/22/typescript-tsc-erased-types-strictness</id><content type="html" xml:base="https://0end1.github.io/2026/09/22/typescript-tsc-erased-types-strictness/"><![CDATA[<h2 id="开篇一位身兼两职的翻译官">开篇：一位身兼两职的翻译官</h2>

<p>上一篇我们搞清了 TypeScript 的立场：一个在代码运行前工作的静态检查器。但有个问题悬而未决——浏览器根本不认识 <code class="language-plaintext highlighter-rouge">.ts</code> 文件，你的类型注解要送到哪里去？</p>

<p>答案是 <code class="language-plaintext highlighter-rouge">tsc</code>，TypeScript 编译器。它像一位身兼两职的翻译官：<strong>第一职是安检</strong>——在翻译前把你的稿子从头到尾审一遍，发现”类型用错了”就当面指出；<strong>第二职才是翻译</strong>——把 TypeScript 代码转写（transform）成等价的 JavaScript，让你能真正跑起来。有意思的是，这两项职责是<strong>解耦</strong>的：安检发现了问题，翻译照做不误；你还可以指定”翻译成哪个年代的 JavaScript”。本篇就把这两条线都走一遍。</p>

<h2 id="一初识-tsc一场什么都没发生的仪式">一、初识 tsc：一场”什么都没发生”的仪式</h2>

<p>先安装再开跑：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>npm <span class="nb">install</span> <span class="nt">-g</span> typescript
</pre></td></tr></tbody></table></code></pre></div></div>

<p>进入空文件夹，写下第一个 TypeScript 程序 <code class="language-plaintext highlighter-rouge">hello.ts</code>：</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">// Greets the world.</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello world!</span><span class="dl">"</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>没错——它和 JavaScript 写法<strong>一模一样</strong>，没有任何花哨的东西。运行 <code class="language-plaintext highlighter-rouge">tsc hello.ts</code>，控制台一片寂静，什么也没输出。这是因为它没发现类型错误，自然无话可说；但回头一看，目录里多了个 <code class="language-plaintext highlighter-rouge">hello.js</code>——这就是编译产物，内容几乎和源文件相同。</p>

<p>这里藏着一个容易被忽略的设计目标：<strong>tsc 努力输出”看起来像人写的”干净代码</strong>——缩进一致、尊重换行、尽量保留注释。它不是把你的代码搅碎重排，而是做一次礼貌的转写。</p>

<p>那么，如果代码真有问题呢？把 <code class="language-plaintext highlighter-rouge">hello.ts</code> 改成：</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1">// This is an industrial-grade general-purpose greeter function:</span>
<span class="kd">function</span> <span class="nx">greet</span><span class="p">(</span><span class="nx">person</span><span class="p">,</span> <span class="nx">date</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Hello </span><span class="p">${</span><span class="nx">person</span><span class="p">}</span><span class="s2">, today is </span><span class="p">${</span><span class="nx">date</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="p">}</span>

<span class="nx">greet</span><span class="p">(</span><span class="dl">"</span><span class="s2">Brendan</span><span class="dl">"</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>再跑 <code class="language-plaintext highlighter-rouge">tsc hello.ts</code>，这次命令行报错了：</p>

<pre><code class="language-txt">Expected 2 arguments, but got 1.
</code></pre>

<p>注意一个细节：到目前为止我们写的<strong>全是标准 JavaScript</strong>，一个类型注解都没加，但类型检查照样抓出了漏传参数的 bug。这就是上一说的延续——TypeScript 的检查能力从 JavaScript 代码本身的形状开始生效。</p>

<h2 id="二带错也照常输出typescript-的核心价值观">二、带错也照常输出：TypeScript 的核心价值观</h2>

<p>刚才的例子还有个更微妙的现象：虽然报了错，<code class="language-plaintext highlighter-rouge">hello.js</code> <strong>依然被更新了</strong>。查错归查错，翻译照做。</p>

<p>这不是 bug，而是 TypeScript 的一条核心价值观：<strong>大多数时候，你比 TypeScript 更清楚自己在干什么</strong>。最典型的场景是从 JavaScript 迁移到 TypeScript：代码本来跑得好好的，一转成 <code class="language-plaintext highlighter-rouge">.ts</code> 冒出一堆类型错误，难道迁移就该让程序停下来吗？不该。所以 tsc 默认”报错但不拦路”。</p>

<p>当你想更严格时，有一条开关可用：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>tsc <span class="nt">--noEmitOnError</span> hello.ts
</pre></td></tr></tbody></table></code></pre></div></div>

<p>加上这个标志后，只要存在类型错误，<code class="language-plaintext highlighter-rouge">hello.js</code> 就<strong>永远不会被更新</strong>。两种策略的取舍如下：</p>

<table>
  <thead>
    <tr>
      <th>策略</th>
      <th>行为</th>
      <th>适用场景</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>默认</td>
      <td>报错，但照常输出 JS</td>
      <td>渐进迁移、原型探索</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--noEmitOnError</code></td>
      <td>有错就不输出</td>
      <td>严谨项目、CI 构建</td>
    </tr>
  </tbody>
</table>

<h2 id="三显式类型与推断什么时候该写注解">三、显式类型与推断：什么时候该写注解</h2>

<p>回到 <code class="language-plaintext highlighter-rouge">greet</code>，给它补上类型：</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nx">greet</span><span class="p">(</span><span class="nx">person</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">date</span><span class="p">:</span> <span class="nb">Date</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Hello </span><span class="p">${</span><span class="nx">person</span><span class="p">}</span><span class="s2">, today is </span><span class="p">${</span><span class="nx">date</span><span class="p">.</span><span class="nx">toDateString</span><span class="p">()}</span><span class="s2">!`</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">: string</code> 和 <code class="language-plaintext highlighter-rouge">: Date</code> 叫<strong>类型注解（type annotations）</strong>，读作”greet 接收一个 string 类型的 person 和一个 Date 类型的 date”。有了它，TypeScript 立刻能发现另一处调用错误：</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nx">greet</span><span class="p">(</span><span class="dl">"</span><span class="s2">Maddison</span><span class="dl">"</span><span class="p">,</span> <span class="nb">Date</span><span class="p">());</span> <span class="c1">// 报错！</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这个报错相当反直觉——<code class="language-plaintext highlighter-rouge">Date()</code> 不就是造日期吗？还真不是。<strong>不带 <code class="language-plaintext highlighter-rouge">new</code> 直接调用 <code class="language-plaintext highlighter-rouge">Date()</code> 返回的是字符串</strong>，要拿到 Date 对象必须写 <code class="language-plaintext highlighter-rouge">new Date()</code>。没有类型系统时，这个坑要等运行时才炸；现在编辑器当场标红。</p>

<p>不过 Handbook 紧接着泼了盆”别过度”的冷水：</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="kd">let</span> <span class="nx">msg</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">hello there!</span><span class="dl">"</span><span class="p">;</span>
<span class="c1">//  ^? 鼠标悬停可见类型是 string</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>即使不写注解，TypeScript 也能<strong>推断（infer）</strong>出 <code class="language-plaintext highlighter-rouge">msg</code> 是 string。它的建议很明确：<strong>如果类型系统推出来的结果和你打算写的注解一模一样，那就别写了</strong>——注解应该用在你需要”说出意图”的地方（比如函数参数），而不是给每个变量都贴标签。</p>

<h2 id="四类型擦除与降级编译注解去了哪">四、类型擦除与降级编译：注解去了哪</h2>

<p>把带注解的 <code class="language-plaintext highlighter-rouge">greet</code> 交给 tsc，输出是：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nx">greet</span><span class="p">(</span><span class="nx">person</span><span class="p">,</span> <span class="nx">date</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello </span><span class="dl">"</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="nx">person</span><span class="p">,</span> <span class="dl">"</span><span class="s2">, today is </span><span class="dl">"</span><span class="p">).</span><span class="nx">concat</span><span class="p">(</span><span class="nx">date</span><span class="p">.</span><span class="nx">toDateString</span><span class="p">(),</span> <span class="dl">"</span><span class="s2">!</span><span class="dl">"</span><span class="p">));</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>和源码相比有两处变化，各代表一条重要机制：</p>

<p><strong>变化一：注解全部消失</strong>。<code class="language-plaintext highlighter-rouge">person: string</code> 变回了 <code class="language-plaintext highlighter-rouge">person</code>。因为类型注解不是 JavaScript（严谨地说是 ECMAScript）的一部分，没有任何浏览器能直接运行它——这正是 TypeScript 需要编译器的根本原因：<strong>把 TypeScript 特有的代码剥掉或转写掉</strong>。大多数 TS 特有代码都会被这样”擦除”（erased）。Handbook 为此给出了一句值得抄在显眼处的备忘：</p>

<blockquote>
  <p><strong>记住：类型注解永远不会改变程序的运行时行为。</strong></p>
</blockquote>

<p>这句话是理解 TypeScript 性质的钥匙——它加的是”安检”，不是”发动机”。指望用类型做运行时校验（比如校验接口返回值）是常见误区。</p>

<p><strong>变化二：模板字符串被改写成 <code class="language-plaintext highlighter-rouge">.concat()</code> 拼接</strong>。这是<strong>降级编译（downleveling）</strong>：把新版 ECMAScript 的语法改写成旧版（如 ES3/ES5）能跑的形式。默认 target 是 ES5——一个极其古老的版本。可以用 <code class="language-plaintext highlighter-rouge">--target</code> 指定：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>tsc <span class="nt">--target</span> es2015 hello.ts
</pre></td></tr></tbody></table></code></pre></div></div>

<p>输出就保留了原生模板字符串。Handbook 顺带提醒：绝大多数现代浏览器都支持 ES2015，<strong>除非要兼容古老浏览器，否则放心把 target 设为 ES2015 或更高</strong>。</p>

<h2 id="五严格性旋钮从开关到旋钮">五、严格性旋钮：从”开关”到”旋钮”</h2>

<p>最后一个话题是所有 TypeScript 项目的灵魂设置。不同用户对检查器的期待不同：</p>

<ul>
  <li>有人要<strong>宽松</strong>：类型可选、推断取最宽松的类型、不检查 <code class="language-plaintext highlighter-rouge">null</code>/<code class="language-plaintext highlighter-rouge">undefined</code>——这是默认体验，专为”别挡我的路”设计，迁移老项目时是理想的起点；</li>
  <li>有人要<strong>严格</strong>：让 TypeScript 一开始就尽力验证一切。</li>
</ul>

<p>关键洞察在于：<strong>严格性不是一个开关，而是一个旋钮</strong>。拧得越紧，TypeScript 替你检查得越多，代价是需要额外的工作——但长期看是划算的，还能换来更精确的工具支持。新代码库应当<strong>始终把严格检查打开</strong>。</p>

<p><code class="language-plaintext highlighter-rouge">tsconfig.json</code> 里的一句 <code class="language-plaintext highlighter-rouge">"strict": true</code>（或命令行的 <code class="language-plaintext highlighter-rouge">--strict</code>）能同时拧开所有严格选项，也可以逐个单独关闭。其中最重要的两个：</p>

<table>
  <thead>
    <tr>
      <th>选项</th>
      <th>解决什么问题</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">noImplicitAny</code></td>
      <td>有些地方 TS 推断不出类型会退回最宽松的 <code class="language-plaintext highlighter-rouge">any</code>（等于回到纯 JavaScript 体验）；开启后，任何被<strong>隐式</strong>推断为 <code class="language-plaintext highlighter-rouge">any</code> 的变量都会报错</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">strictNullChecks</code></td>
      <td>默认 <code class="language-plaintext highlighter-rouge">null</code>/<code class="language-plaintext highlighter-rouge">undefined</code> 可以赋给任何类型，方便但危险——忘了处理它们是无数线上事故的根源，被称为”十亿美元的错误”（billion dollar mistake）；开启后必须显式处理这两种值</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">any</code> 的问题说得再直白些：用了它，TypeScript 对那个值就”失明”了——检查和补全都失效，用得越多，用 TypeScript 的意义越少。</p>

<h2 id="实践建议">实践建议</h2>

<ol>
  <li><strong>新项目一律 <code class="language-plaintext highlighter-rouge">"strict": true</code></strong>：这是 Handbook 的原话级建议（”新代码库应当始终开启严格检查”）。迁移老项目可以先默认宽松跑通，再逐步拧紧旋钮。</li>
  <li><strong>注解写在”刀刃”上</strong>：函数参数、公开 API 边界处写注解；局部变量交给推断。判断标准很简单——推出来的类型和你要写的一样吗？一样就删掉注解。</li>
  <li><strong>记牢两个”反直觉”</strong>：<code class="language-plaintext highlighter-rouge">Date()</code> 返回字符串、类型注解不影响运行时行为。前者是日常 bug 源，后者决定了”运行时校验请用 zod 之类的库，别指望类型系统”。</li>
</ol>

<h2 id="小结">小结</h2>

<p>本篇揭开了编译器的面纱：<code class="language-plaintext highlighter-rouge">tsc</code> 是安检员兼翻译官——检查代码类型，并把 <code class="language-plaintext highlighter-rouge">.ts</code> 转写成 JS；默认”报错也照常输出”体现的是”你比它更懂”的价值观，<code class="language-plaintext highlighter-rouge">--noEmitOnError</code> 可以关掉这份宽容。类型注解在编译时被<strong>擦除</strong>，永远不改变运行时行为；旧语法会被<strong>降级编译</strong>成 target 版本可跑的形式。最后，严格性是旋钮不是开关，<code class="language-plaintext highlighter-rouge">strict</code> 一键全开，<code class="language-plaintext highlighter-rouge">noImplicitAny</code> 堵住 <code class="language-plaintext highlighter-rouge">any</code> 的暗门，<code class="language-plaintext highlighter-rouge">strictNullChecks</code> 管住”十亿美元的错误”。</p>

<p>下一篇进入干货最密的《Everyday Types》上篇：string/number/boolean、数组、声名狼藉的 <code class="language-plaintext highlighter-rouge">any</code>、变量注解，以及函数类型的写法。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="typescript" /><category term="handbook" /><category term="tsc" /><category term="type-erasure" /><category term="downleveling" /><category term="strictness" /><summary type="html"><![CDATA[开篇：一位身兼两职的翻译官]]></summary></entry><entry><title type="html">Rust 学习笔记（21/21 完结）：最后的项目——亲手撸一个多线程 Web 服务器</title><link href="https://0end1.github.io/2026/09/21/rust-final-project-web-server/" rel="alternate" type="text/html" title="Rust 学习笔记（21/21 完结）：最后的项目——亲手撸一个多线程 Web 服务器" /><published>2026-09-21T09:00:00+08:00</published><updated>2026-09-21T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/21/rust-final-project-web-server</id><content type="html" xml:base="https://0end1.github.io/2026/09/21/rust-final-project-web-server/"><![CDATA[<h2 id="开篇毕业考试">开篇：毕业考试</h2>

<p>这是本系列的最后一篇，也是《Rust 程序设计语言》的最后一章。前二十一篇笔记里，所有权、借用、trait、生命周期、并发、模式匹配轮番登场，但都停留在「知识点」层面——而第 20 章是毕业考试：<strong>只靠标准库，手写一个多线程 Web 服务器</strong>。</p>

<p>书里特意交代了一个细节：crates.io 上有大量生产级 Web 框架，我们这样做<strong>不是最佳实践，而是学习</strong>。因为 Rust 是系统编程语言，你可以选择抽象层次——自己写 HTTP server 和线程池，才能看懂将来用的 crate 背后的通用理念。就像练厨艺先不点外卖：外卖能吃饱，但只有亲手颠勺才知道火候是什么。</p>

<p>整个项目分三阶段演进，每一步都踩在前面的知识点上：</p>

<table>
  <thead>
    <tr>
      <th>阶段</th>
      <th>核心问题</th>
      <th>用到的知识</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>单线程服务器</td>
      <td>听连接、读请求、写响应</td>
      <td>TCP/HTTP 协议、I/O、字符串</td>
    </tr>
    <tr>
      <td>多线程化</td>
      <td>慢请求阻塞一切 → 线程池</td>
      <td>通道、<code class="language-plaintext highlighter-rouge">Arc&lt;Mutex&lt;T&gt;&gt;</code>、闭包、trait 对象</td>
    </tr>
    <tr>
      <td>优雅停机</td>
      <td>ctrl-c 暴力退出 → 主动清理</td>
      <td><code class="language-plaintext highlighter-rouge">Drop</code>、<code class="language-plaintext highlighter-rouge">Option::take</code>、枚举消息</td>
    </tr>
  </tbody>
</table>

<h2 id="一单线程服务器40-行代码看懂-http">一、单线程服务器：40 行代码看懂 HTTP</h2>

<p>Web 服务器的两个主角协议：<strong>TCP</strong>（底层，描述信息如何从一台机器到另一台）和 <strong>HTTP</strong>（构建于 TCP 之上，定义请求和响应的内容）。两者都是请求-响应协议——客户端发起，服务端监听并回应。我们要做的，就是处理这些「原始字节数据」。</p>

<p>第一步只需要标准库的 <code class="language-plaintext highlighter-rouge">std::net</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">net</span><span class="p">::</span><span class="n">TcpListener</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">listener</span> <span class="o">=</span> <span class="nn">TcpListener</span><span class="p">::</span><span class="nf">bind</span><span class="p">(</span><span class="s">"127.0.0.1:7878"</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">for</span> <span class="n">stream</span> <span class="k">in</span> <span class="n">listener</span><span class="nf">.incoming</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">stream</span> <span class="o">=</span> <span class="n">stream</span><span class="nf">.unwrap</span><span class="p">();</span>
        <span class="nf">handle_connection</span><span class="p">(</span><span class="n">stream</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>几个易忽略的细节：<code class="language-plaintext highlighter-rouge">7878</code> 在九宫格电话键盘上打出来就是 “rust”（这是选端口的彩蛋）；<code class="language-plaintext highlighter-rouge">incoming</code> 迭代的其实是<strong>连接尝试</strong>而非连接本身（系统可能限制同时打开的连接数）；<code class="language-plaintext highlighter-rouge">handle_connection</code> 里的 <code class="language-plaintext highlighter-rouge">stream</code> 必须是 <code class="language-plaintext highlighter-rouge">mut</code>——<code class="language-plaintext highlighter-rouge">TcpStream</code> 内部会缓存读取的多余数据，<strong>读操作也需要可变性</strong>，这点很反直觉。</p>

<p><code class="language-plaintext highlighter-rouge">handle_connection</code> 用 1024 字节的缓冲区读入请求后打印，就能看到浏览器发来的原始 HTTP 请求：请求行 <code class="language-plaintext highlighter-rouge">GET / HTTP/1.1</code>（方法 + URI + 版本，以 CRLF 即 <code class="language-plaintext highlighter-rouge">\r\n</code> 结尾）+ 一堆 header。手写响应同样简单——<code class="language-plaintext highlighter-rouge">"HTTP/1.1 200 OK\r\n\r\n"</code> 这个微型字符串写回流里，浏览器就不再报错；再配合 <code class="language-plaintext highlighter-rouge">fs::read_to_string</code> 把 <code class="language-plaintext highlighter-rouge">hello.html</code> 塞进 body，用 <code class="language-plaintext highlighter-rouge">Content-Length</code> 标头声明长度，一个真正的 HTML 页面就渲染出来了。</p>

<p>最后用 <code class="language-plaintext highlighter-rouge">buffer.starts_with(b"GET / HTTP/1.1\r\n")</code> 区分 <code class="language-plaintext highlighter-rouge">/</code> 与其他请求（后者返回 404），并做一次经典重构：把 <code class="language-plaintext highlighter-rouge">if/else</code> 收缩为只返回<strong>元组</strong> <code class="language-plaintext highlighter-rouge">("HTTP/1.1 200 OK", "hello.html")</code>，再用模式解构赋值，读文件和写响应的代码只保留一份。重复代码消失了，两种情况的差异一目了然。</p>

<h2 id="二线程池编译器驱动的开发">二、线程池：编译器驱动的开发</h2>

<p>单线程服务器的致命弱点用一个 <code class="language-plaintext highlighter-rouge">/sleep</code> 请求（休眠 5 秒）就能验证：先请求 <code class="language-plaintext highlighter-rouge">/sleep</code> 再请求 <code class="language-plaintext highlighter-rouge">/</code>，后者要干等 5 秒——因为服务器<strong>串行处理连接</strong>，慢请求把队伍全堵死了。</p>

<p>最朴素的修复是每个连接 <code class="language-plaintext highlighter-rouge">thread::spawn</code> 一个新线程，但这会无限制创建线程——千万级请求打过来直接耗尽资源，这就是拒绝服务（DoS）攻击。所以答案是<strong>线程池</strong>：固定数量的线程预先待命，新请求进入队列，空闲线程认领任务，处理完继续待命。可以并发处理 N 个请求（N 为线程数）。</p>

<p>有意思的是开发方式：书里先写出<strong>假想的调用方代码</strong>（<code class="language-plaintext highlighter-rouge">ThreadPool::new(4)</code> + <code class="language-plaintext highlighter-rouge">pool.execute(闭包)</code>），再让 <code class="language-plaintext highlighter-rouge">cargo check</code> 的编译错误一步步指导实现——作者称之为<strong>编译器驱动开发</strong>。因为线程池与 Web 服务器业务无关，还被拆进了独立的库 crate（<code class="language-plaintext highlighter-rouge">src/lib.rs</code>），主程序挪到 <code class="language-plaintext highlighter-rouge">src/bin/main.rs</code>。</p>

<p>实现中藏着三个关键决策：</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">execute</code> 的闭包 bound 抄谁？</strong> 抄 <code class="language-plaintext highlighter-rouge">thread::spawn</code> 的签名：<code class="language-plaintext highlighter-rouge">F: FnOnce() + Send + 'static</code>——任务只执行一次所以 <code class="language-plaintext highlighter-rouge">FnOnce</code>，要跨线程转移所以 <code class="language-plaintext highlighter-rouge">Send</code>，不知道线程跑多久所以 <code class="language-plaintext highlighter-rouge">'static</code>。</li>
  <li><strong>谁来领任务？</strong> <code class="language-plaintext highlighter-rouge">ThreadPool</code> 不直接存线程，而是存 <code class="language-plaintext highlighter-rouge">Worker</code>（每个 Worker 包着一个 <code class="language-plaintext highlighter-rouge">JoinHandle</code>）——类比餐馆厨房的员工：先上岗，等订单来了再做菜。因为标准库只能「创建线程即给代码」，而我们需要「先创建线程，后发代码」，这层封装必须自己做。</li>
  <li><strong>任务怎么发？</strong> 用第 16 章的 <code class="language-plaintext highlighter-rouge">mpsc</code> 通道充当任务队列。但直接把 receiver 传给多个 Worker 会报 E0382——通道是<strong>多生产者、单消费者</strong>的。解法是 <code class="language-plaintext highlighter-rouge">Arc::new(Mutex::new(receiver))</code>：<code class="language-plaintext highlighter-rouge">Arc</code> 让多个 Worker 共享 receiver 的所有权，<code class="language-plaintext highlighter-rouge">Mutex</code> 保证一次只有一个 Worker 取任务。</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">Job</code> 的定义是第 19 章类型别名的实战应用：<code class="language-plaintext highlighter-rouge">type Job = Box&lt;dyn FnOnce() + Send + 'static&gt;;</code>——把闭包装箱成 trait 对象，再取个短名字在通道里传递。</p>

<h3 id="一个精妙的陷阱while-let-vs-loop">一个精妙的陷阱：<code class="language-plaintext highlighter-rouge">while let</code> vs <code class="language-plaintext highlighter-rouge">loop</code></h3>

<p>第 18 章学过 <code class="language-plaintext highlighter-rouge">while let</code>，很自然会想这样写 Worker：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">while</span> <span class="k">let</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">job</span><span class="p">)</span> <span class="o">=</span> <span class="n">receiver</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.recv</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">job</span><span class="p">();</span>  <span class="c1">// ⚠️ 锁还握着！</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>能编译、能运行，但<strong>并发性悄悄失效</strong>：<code class="language-plaintext highlighter-rouge">Mutex</code> 没有公有的 <code class="language-plaintext highlighter-rouge">unlock</code> 方法，锁的释放靠 <code class="language-plaintext highlighter-rouge">lock</code> 返回的 <code class="language-plaintext highlighter-rouge">MutexGuard</code> 被丢弃——而 <code class="language-plaintext highlighter-rouge">while</code> 条件表达式的值在整个循环体中都活着，<code class="language-plaintext highlighter-rouge">job()</code> 执行期间锁一直被持有，其他 Worker 全在干等。改成 <code class="language-plaintext highlighter-rouge">loop</code> + 循环体内的 <code class="language-plaintext highlighter-rouge">let job = receiver.lock().unwrap().recv().unwrap();</code>，<code class="language-plaintext highlighter-rouge">MutexGuard</code> 在这条 <code class="language-plaintext highlighter-rouge">let</code> 语句结束（<code class="language-plaintext highlighter-rouge">job()</code> 执行前）就被丢弃，锁立刻释放。<strong>一个变量的生命周期细节，决定了服务器是「假并发」还是真并发</strong>。</p>

<h2 id="三优雅停机dropterminate-与两段循环">三、优雅停机：Drop、Terminate 与两段循环</h2>

<p>ctrl-C 终止主线程时，所有 Worker 会被立刻掐断，哪怕正在处理请求——这不体面。真正的服务器要做到<strong>优雅停机</strong>（graceful shutdown）：处理完手头的活，再干净地退出。</p>

<p><strong>第一步：实现 <code class="language-plaintext highlighter-rouge">Drop</code></strong>。直觉写法是在 <code class="language-plaintext highlighter-rouge">drop</code> 里对每个 Worker 调用 <code class="language-plaintext highlighter-rouge">thread.join()</code>，但编译器报 E0507：我们手里只有 <code class="language-plaintext highlighter-rouge">&amp;mut Worker</code>，而 <code class="language-plaintext highlighter-rouge">join</code> 要<strong>拿走所有权</strong>。解法正是第 17 章的模式——把字段类型改成 <code class="language-plaintext highlighter-rouge">Option&lt;thread::JoinHandle&lt;()&gt;&gt;</code>，清理时用 <code class="language-plaintext highlighter-rouge">take()</code> 把值移出来：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">thread</span><span class="p">)</span> <span class="o">=</span> <span class="n">worker</span><span class="py">.thread</span><span class="nf">.take</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">thread</span><span class="nf">.join</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>第二步：让线程停下来</strong>。光 <code class="language-plaintext highlighter-rouge">join</code> 没用——Worker 还在 <code class="language-plaintext highlighter-rouge">loop</code> 里等任务，主线程会永远阻塞。于是通道不再只发任务，而是发一个枚举：<code class="language-plaintext highlighter-rouge">enum Message { NewJob(Job), Terminate }</code>。Worker 收到 <code class="language-plaintext highlighter-rouge">NewJob</code> 就干活，收到 <code class="language-plaintext highlighter-rouge">Terminate</code> 就 <code class="language-plaintext highlighter-rouge">break</code> 退出循环。</p>

<p><strong>第三步：两段循环，避免死锁</strong>。<code class="language-plaintext highlighter-rouge">Drop</code> 里先向通道发 <code class="language-plaintext highlighter-rouge">size</code> 条 <code class="language-plaintext highlighter-rouge">Terminate</code>，再在<strong>另一个循环</strong>里逐个 <code class="language-plaintext highlighter-rouge">join</code>。为什么不能合成一个循环？书里的推演很精彩：假设只有两个 Worker，若边发终止消息边 join，第一个 Worker 正忙着处理请求时，<code class="language-plaintext highlighter-rouge">Terminate</code> 可能被第二个 Worker 抢走了——于是主线程永远等第一个 Worker 结束，而它永远等不到自己的终止消息。<strong>死锁</strong>。先全员广播、再统一收队，才保证每个线程都能收到自己的「下班通知」。</p>

<p>在 <code class="language-plaintext highlighter-rouge">main</code> 里用 <code class="language-plaintext highlighter-rouge">listener.incoming().take(2)</code> 只处理两个请求验证整套机制：第三个请求失败后打印 <code class="language-plaintext highlighter-rouge">Shutting down.</code>，随后是 <code class="language-plaintext highlighter-rouge">Terminate</code> 广播与逐个 worker 关闭的日志——服务器体面谢幕。</p>

<h2 id="四全书总结这套项目浓缩了什么">四、全书总结：这套项目浓缩了什么</h2>

<table>
  <thead>
    <tr>
      <th>项目组件</th>
      <th>对应章节知识</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TcpListener</code>/<code class="language-plaintext highlighter-rouge">TcpStream</code> 读写</td>
      <td>第 12 章 I/O、第 3 章类型</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">if buffer.starts_with(...)</code> + 元组解构</td>
      <td>第 18 章模式匹配</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">execute</code> 的 <code class="language-plaintext highlighter-rouge">FnOnce + Send + 'static</code></td>
      <td>第 13 章闭包、第 19 章 trait bound</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Box&lt;dyn FnOnce()&gt;</code> 任务装箱</td>
      <td>第 17 章 trait 对象、第 19 章类型别名</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mpsc::channel</code> + <code class="language-plaintext highlighter-rouge">Arc&lt;Mutex&lt;Receiver&gt;&gt;</code></td>
      <td>第 16 章并发与共享状态</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Option::take</code> 移出所有权</td>
      <td>第 17 章模式、第 6 章 <code class="language-plaintext highlighter-rouge">Option</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">impl Drop for ThreadPool</code></td>
      <td>第 15 章智能指针与 RAII</td>
    </tr>
    <tr>
      <td>编译器驱动开发 + <code class="language-plaintext highlighter-rouge">assert!</code>/文档注释</td>
      <td>第 9 章错误处理、第 14 章文档</td>
    </tr>
  </tbody>
</table>

<p><strong>实践建议</strong>：</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">while let</code> 陷阱要刻进肌肉记忆</strong>：凡是用 <code class="language-plaintext highlighter-rouge">MutexGuard</code>，警惕它被 <code class="language-plaintext highlighter-rouge">while</code> 条件、临时变量拖着「多活」几行——锁的持有时间越短越好。</li>
  <li><strong>API 先行，实现随后</strong>：先写出期望的调用代码再动手实现，编译器的报错序列就是你的任务清单；通用逻辑（线程池）与业务（服务器）拆成独立 crate。</li>
  <li><strong>优雅停机是生产素养</strong>：<code class="language-plaintext highlighter-rouge">Drop</code> 清理 + 显式退出信号是通配模式，不只是 Web 服务器专用——任何「长期运行的 worker 集合」都适用。</li>
</ol>

<p><strong>完结感言</strong>：从 <code class="language-plaintext highlighter-rouge">cargo new</code> 到一个能优雅停机的多线程 Web 服务器，21 篇笔记走完了《Rust 程序设计语言》全程。Rust 的学习曲线陡在前期——所有权、借用、生命周期在第一周就能劝退——但收益同样前置：一旦编译通过，运行时的心智负担骤降。书末作者说得好：你已经准备好实现自己的项目了，也别忘了社区里乐于助人的 Rustaceans。下一次遇到问题，去翻文档、读源码、提问吧——这门语言的下一个六年，有你一份。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="rust" /><category term="web-server" /><category term="thread-pool" /><category term="graceful-shutdown" /><category term="series-final" /><summary type="html"><![CDATA[开篇：毕业考试]]></summary></entry><entry><title type="html">TypeScript 学习笔记（1/16）：开篇——类型是什么，静态检查到底在查什么</title><link href="https://0end1.github.io/2026/09/21/typescript-static-type-checking/" rel="alternate" type="text/html" title="TypeScript 学习笔记（1/16）：开篇——类型是什么，静态检查到底在查什么" /><published>2026-09-21T09:00:00+08:00</published><updated>2026-09-21T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/21/typescript-static-type-checking</id><content type="html" xml:base="https://0end1.github.io/2026/09/21/typescript-static-type-checking/"><![CDATA[<h2 id="开篇给-javascript-装一盏前置探照灯">开篇：给 JavaScript 装一盏「前置探照灯」</h2>

<p>想象你在一条没有路灯的山路上夜跑。JavaScript 的开发体验就是这样——你可以跑得飞快，但脚下的坑只有踩上去才知道。脚本跑起来、用户点下去、线上崩了，控制台甩给你一句 <code class="language-plaintext highlighter-rouge">TypeError: xxx is not a function</code>，这时你才知道：哦，这里传错了。</p>

<p>TypeScript 做的事，是在你出发前先派一架无人机把这条路照一遍。它不替你跑，也不改变路线，只是提前告诉你「第 3 公里有个坑」。官方 Handbook 对它的定义非常克制：<strong>TypeScript 是一个 JavaScript 程序的静态类型检查器（static typechecker）</strong>——在你代码运行之前（static）运行，确保程序里的类型是正确的一（typechecked）。</p>

<p>这一篇是系列的开场，我们要回答三个问题：这本 Handbook 讲了什么、类型到底是什么、以及 TypeScript 究竟能在运行前抓住哪些错误。</p>

<h2 id="一handbook-的定位指南不是规范">一、Handbook 的定位：指南，不是规范</h2>

<p>Handbook 开篇就把边界说清楚了，这比急着学语法更重要：</p>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>Handbook</th>
      <th>Reference（参考页）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>目标</td>
      <td>面向日常开发者的完整指南</td>
      <td>对单个概念的深入解释</td>
    </tr>
    <tr>
      <td>读法</td>
      <td>从左到右按顺序读</td>
      <td>可随意跳读，不追求连续性</td>
    </tr>
    <tr>
      <td>覆盖度</td>
      <td>讲清主要特性与行为，略过边角</td>
      <td>精确、形式化地描述行为</td>
    </tr>
  </tbody>
</table>

<p>读完之后你应该能做到三件事：<strong>读懂常见的 TypeScript 语法与模式</strong>、<strong>解释重要编译选项的作用</strong>、<strong>在大多数情况下正确预测类型系统的行为</strong>。</p>

<p>同时它明确列出了「非目标」（Non-Goals），很有意思：</p>

<ul>
  <li>不从头教 JavaScript 基础（函数、类、闭包）——需要时会给外链；</li>
  <li>不是语言规范——为了可读性会跳过形式化描述和边角案例；</li>
  <li>不讲与构建工具的集成（webpack、babel、react、vue……）——那些在别处。</li>
</ul>

<p>也就是说，这是一本「几小时能读完」的务实指南，而不是一本字典。</p>

<h2 id="二类型是什么从-messagetolowercase-说起">二、类型是什么：从 <code class="language-plaintext highlighter-rouge">message.toLowerCase()</code> 说起</h2>

<p>Handbook 的第一课不是语法，而是一个思想实验。看这段 JavaScript：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nx">message</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span> <span class="c1">// 访问属性并调用</span>
<span class="nx">message</span><span class="p">();</span>             <span class="c1">// 直接调用</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如果不知道 <code class="language-plaintext highlighter-rouge">message</code> 是什么值，没人能断言这两行的结果。它在运行时取决于四个问题：<code class="language-plaintext highlighter-rouge">message</code> 可调用吗？它有 <code class="language-plaintext highlighter-rouge">toLowerCase</code> 属性吗？该属性可调用吗？返回值又是什么？</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Hello World!</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">message</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span> <span class="c1">// 正常，返回 "hello world!"</span>
<span class="nx">message</span><span class="p">();</span>             <span class="c1">// TypeError: message is not a function</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>关键点在于：<strong>JavaScript 运行时是靠「值的类型」来决定行为的</strong>。对 <code class="language-plaintext highlighter-rouge">string</code>、<code class="language-plaintext highlighter-rouge">number</code> 这类原始类型，我们还能用 <code class="language-plaintext highlighter-rouge">typeof</code> 在运行时问一句；但对函数这种东西，运行时根本没有对应的机制来告诉你「这个参数需要有 <code class="language-plaintext highlighter-rouge">flip</code> 方法」：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nx">fn</span><span class="p">(</span><span class="nx">x</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">x</span><span class="p">.</span><span class="nx">flip</span><span class="p">();</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这段代码读起来很清楚——<code class="language-plaintext highlighter-rouge">x</code> 必须是一个带 <code class="language-plaintext highlighter-rouge">flip</code> 方法的对象。但纯 JavaScript 里，唯一确认它能不能跑的办法就是<strong>真的去调用它</strong>。这就是<strong>动态类型</strong>：运行代码，然后看会发生什么。</p>

<p>TypeScript 给出的另一种选择是<strong>静态类型系统</strong>：在代码运行之前，就预测它应该做什么。把那句拗口的话翻译过来——<strong>类型，就是描述「哪些值可以传给 <code class="language-plaintext highlighter-rouge">fn</code>、哪些会崩」的一套语言</strong>。</p>

<h2 id="三静态类型检查把-bug-挪到运行之前">三、静态类型检查：把 bug 挪到运行之前</h2>

<p>为什么不早点发现？Handbook 给的理由很实在：就算你改完代码立刻重跑，也可能没测到那条分支；就算侥幸撞上了，中间可能已经堆了一大堆新代码，排查成本极高。</p>

<p>于是，<code class="language-plaintext highlighter-rouge">const message = "hello!"; message();</code> 这段，TypeScript 在你保存文件的那一刻就报 <code class="language-plaintext highlighter-rouge">This expression is not callable</code>（错误码 2349）——<strong>代码还没跑，问题已经出现在编辑器里</strong>。</p>

<h2 id="四真正的杀手锏非异常失败non-exception-failures">四、真正的杀手锏：非异常失败（Non-exception Failures）</h2>

<p>这是本篇最值得记住的一节。前面讲的都是「运行时会抛错」的场景，但 JavaScript 里有大量<strong>不抛异常、却明显是 bug</strong> 的代码。ECMAScript 规范规定：调用不可调用的东西要抛错；但访问对象上不存在的属性，返回的是 <code class="language-plaintext highlighter-rouge">undefined</code>。</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Daniel</span><span class="dl">"</span><span class="p">,</span> <span class="na">age</span><span class="p">:</span> <span class="mi">26</span> <span class="p">};</span>
<span class="nx">user</span><span class="p">.</span><span class="nx">location</span><span class="p">;</span> <span class="c1">// 不报错，返回 undefined</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>TypeScript 会明确报错：<code class="language-plaintext highlighter-rouge">Property 'location' does not exist</code>（2339）。<strong>它把「合法但可疑」的 JavaScript 也纳入了检查范围</strong>——这是一种取舍：牺牲一点表达自由度，换取大量真实 bug 被提前拦下。Handbook 举了三类典型：</p>

<table>
  <thead>
    <tr>
      <th>错误类型</th>
      <th>示例代码</th>
      <th>TypeScript 的判断</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>拼写错误（typo）</td>
      <td><code class="language-plaintext highlighter-rouge">announcement.toLocalLowerCase()</code></td>
      <td>方法不存在，直接标红（正确写法是 <code class="language-plaintext highlighter-rouge">toLocaleLowerCase</code>）</td>
    </tr>
    <tr>
      <td>忘记调用函数</td>
      <td><code class="language-plaintext highlighter-rouge">Math.random &lt; 0.5</code></td>
      <td>比较一个函数与数字，无意义（应为 <code class="language-plaintext highlighter-rouge">Math.random()</code>）</td>
    </tr>
    <tr>
      <td>基础逻辑错误</td>
      <td><code class="language-plaintext highlighter-rouge">value !== "a"</code> 与 <code class="language-plaintext highlighter-rouge">value === "b"</code> 分支</td>
      <td>当 <code class="language-plaintext highlighter-rouge">value</code> 只能是 <code class="language-plaintext highlighter-rouge">"a" \| "b"</code> 时，后者不可达</td>
    </tr>
  </tbody>
</table>

<p>第三类尤其惊艳：</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mf">0.5</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">a</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">b</span><span class="dl">"</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">value</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">a</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// ...</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">value</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">b</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Oops, unreachable</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>TypeScript 知道 <code class="language-plaintext highlighter-rouge">value</code> 的类型是 <code class="language-plaintext highlighter-rouge">"a" | "b"</code>，<code class="language-plaintext highlighter-rouge">else</code> 分支里它必然是 <code class="language-plaintext highlighter-rouge">"a"</code>，所以 <code class="language-plaintext highlighter-rouge">value === "b"</code> 永远不会成立——这种”死的分支”，纯靠肉眼 review 极容易漏掉。</p>

<h2 id="五类型不只是查错还能让你少犯错">五、类型不只是查错，还能让你少犯错</h2>

<p>如果说查错是”事后拦截”，那 tooling 就是”事前预防”。类型检查器既然知道你正在操作的对象有哪些属性，它就能：</p>

<ul>
  <li><strong>自动补全</strong>：输入 <code class="language-plaintext highlighter-rouge">res.sen</code> 时提示 <code class="language-plaintext highlighter-rouge">send</code>；</li>
  <li><strong>快速修复（quick fixes）</strong>：自动改正某些错误；</li>
  <li><strong>重构与导航</strong>：重命名、整理代码、跳转到定义、查找所有引用。</li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">import</span> <span class="nx">express</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">express</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>

<span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">res</span><span class="p">.</span><span class="nx">sen</span> <span class="c1">// ← 编辑器在这里就能提示 send / sendFile / sendStatus</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>这些能力全部构建在同一个类型检查器之上，而且跨平台——你常用的编辑器基本都有 TypeScript 支持。<strong>“类型”在这里从约束变成了生产力工具</strong>：它不只是告诉你哪里错了，还在你写的时候就把正确选项递到手边。</p>

<h2 id="实践建议">实践建议</h2>

<ol>
  <li><strong>先建立心智模型，再记语法</strong>：把”类型 = 描述值能做什么”这句话刻在脑子里，后面学泛型、条件类型时都会轻松很多——它们只是把这句话参数化。</li>
  <li><strong>优先解决”非异常失败”类报错</strong>：<code class="language-plaintext highlighter-rouge">undefined</code> 属性、拼错的方法名、忘加括号的函数调用，这三类是 TypeScript 性价比最高的收益点，迁移老项目时先盯它们。</li>
  <li><strong>让编辑器替你干活</strong>：补全、跳转定义、查找引用、quick fix——这些不是”锦上添花”，而是你付了类型标注成本后应得的回报，值得花时间熟悉编辑器的 TS 快捷键。</li>
</ol>

<h2 id="小结">小结</h2>

<p>Handbook 的开篇没有急着教 <code class="language-plaintext highlighter-rouge">string</code> 和 <code class="language-plaintext highlighter-rouge">number</code>，而是先讲清 TypeScript 的立场：<strong>它是一个在代码运行前工作的静态检查器，目标是抓住”类型用错了”这一类最常见的 bug</strong>。它不仅能拦住会抛异常的错误，更能拦住那些 JavaScript 默许、但显然不对的写法（访问不存在的属性、拼错方法、函数忘了调用、不可达分支）；而它收集到的类型信息，又会立刻变成编辑器的补全与重构能力。</p>

<p>下一篇我们真正上手：<code class="language-plaintext highlighter-rouge">tsc</code> 编译器怎么用、类型注解写了之后去了哪（类型擦除）、模板字符串为什么被改写成 <code class="language-plaintext highlighter-rouge">concat</code>（降级编译），以及 <code class="language-plaintext highlighter-rouge">strict</code>、<code class="language-plaintext highlighter-rouge">noImplicitAny</code>、<code class="language-plaintext highlighter-rouge">strictNullChecks</code> 这三档”严格性旋钮”该怎么拧。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="typescript" /><category term="handbook" /><category term="static-typing" /><category term="tooling" /><category term="series-start" /><summary type="html"><![CDATA[开篇：给 JavaScript 装一盏「前置探照灯」]]></summary></entry><entry><title type="html">Rust 学习笔记（20/21）：高级特征——unsafe、关联类型与宏的进阶工具箱</title><link href="https://0end1.github.io/2026/09/20/rust-advanced-features/" rel="alternate" type="text/html" title="Rust 学习笔记（20/21）：高级特征——unsafe、关联类型与宏的进阶工具箱" /><published>2026-09-20T09:00:00+08:00</published><updated>2026-09-20T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/20/rust-advanced-features</id><content type="html" xml:base="https://0end1.github.io/2026/09/20/rust-advanced-features/"><![CDATA[<h2 id="开篇rust-的地下室工具房">开篇：Rust 的「地下室工具房」</h2>

<p>前十八章学的东西覆盖了 Rust 的绝大多数日常，但总有一些时刻，你会翻进「地下室」找工具：直接跟操作系统打交道、给 <code class="language-plaintext highlighter-rouge">Vec</code> 实现 <code class="language-plaintext highlighter-rouge">Display</code>、看懂别人代码里诡异的 <code class="language-plaintext highlighter-rouge">&lt;Dog as Animal&gt;::baby_name()</code>。本章就是这间地下室——书里也明说了，这些功能「很少会碰到」，但被设计成了一份<strong>遇到未知内容时的参考手册</strong>。</p>

<p>用攀岩打个比方：安全 Rust 是有护栏的栈道，99% 的风景都能到达；<code class="language-plaintext highlighter-rouge">unsafe</code> 则是撤掉护栏的岩壁——有些山（比如与 C 代码交互）根本没有栈道可走，你必须自己系好绳子。本章四大板块：不安全 Rust、高级 trait、高级类型、宏。先看地图：</p>

<table>
  <thead>
    <tr>
      <th>板块</th>
      <th>核心内容</th>
      <th>一句话定位</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>不安全 Rust</td>
      <td><code class="language-plaintext highlighter-rouge">unsafe</code> 五种超能力</td>
      <td>绕过编译器保护，但护栏只是变小、没被拆掉</td>
    </tr>
    <tr>
      <td>高级 trait</td>
      <td>关联类型、默认泛型参数、完全限定语法、父 trait、newtype</td>
      <td>看懂标准库和第三方库的进阶签名</td>
    </tr>
    <tr>
      <td>高级类型</td>
      <td>类型别名、never type、动态大小类型</td>
      <td>理解 <code class="language-plaintext highlighter-rouge">!</code>、<code class="language-plaintext highlighter-rouge">str</code> 和 <code class="language-plaintext highlighter-rouge">Sized</code> 这些「熟面孔」的另一面</td>
    </tr>
    <tr>
      <td>宏</td>
      <td>声明宏 <code class="language-plaintext highlighter-rouge">macro_rules!</code>、三种过程宏</td>
      <td>为写代码而写代码（元编程）</td>
    </tr>
  </tbody>
</table>

<h2 id="一不安全-rust手动挡模式">一、不安全 Rust：手动挡模式</h2>

<p><code class="language-plaintext highlighter-rouge">unsafe</code> 不是「关闭安全检查」，这个澄清很重要：<strong>借用检查器照常工作，引用照常被检查</strong>。它只是额外解锁了五种编译器无法验证内存安全的操作：</p>

<table>
  <thead>
    <tr>
      <th>超能力</th>
      <th>典型场景</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>解引用裸指针（<code class="language-plaintext highlighter-rouge">*const T</code> / <code class="language-plaintext highlighter-rouge">*mut T</code>）</td>
      <td>调用 C 接口、构建借用检查器理解不了的抽象</td>
    </tr>
    <tr>
      <td>调用不安全函数或方法</td>
      <td>函数文档里标明「调用者需满足某契约」</td>
    </tr>
    <tr>
      <td>访问或修改可变静态变量（<code class="language-plaintext highlighter-rouge">static mut</code>）</td>
      <td>全局可变状态（有数据竞争风险，优先用第16章的并发原语）</td>
    </tr>
    <tr>
      <td>实现不安全 trait（<code class="language-plaintext highlighter-rouge">unsafe trait</code> + <code class="language-plaintext highlighter-rouge">unsafe impl</code>）</td>
      <td>如手动标记裸指针类型为 <code class="language-plaintext highlighter-rouge">Send</code>/<code class="language-plaintext highlighter-rouge">Sync</code></td>
    </tr>
    <tr>
      <td>访问 union 字段</td>
      <td>与 C 的联合体交互（Rust 无法保证当前存的是什么类型）</td>
    </tr>
  </tbody>
</table>

<p>裸指针有个容易忽略的细节：<strong>创建裸指针不需要 <code class="language-plaintext highlighter-rouge">unsafe</code>，解引用才需要</strong>。<code class="language-plaintext highlighter-rouge">let r1 = &amp;num as *const i32;</code> 在安全代码里完全合法——创建指针本身无害，访问指向的值才可能出事。而且裸指针允许同一地址同时存在 <code class="language-plaintext highlighter-rouge">*const</code> 和 <code class="language-plaintext highlighter-rouge">*mut</code>，这在引用世界里是借用规则禁止的，也就埋下了数据竞争的种子。</p>

<p>本章最漂亮的例子是 <code class="language-plaintext highlighter-rouge">split_at_mut</code>：把一个 slice 从中间切成两半，各返回一个可变 slice。纯安全 Rust 写不出来（E0499：编译器只看到「同一个 slice 被可变借用两次」，理解不了「两个不重叠的区间」），但加上裸指针和 <code class="language-plaintext highlighter-rouge">unsafe</code> 块就能实现——而函数签名<strong>不带</strong> <code class="language-plaintext highlighter-rouge">unsafe</code>，调用者毫无感知。这就是「<strong>不安全代码的安全抽象</strong>」：<code class="language-plaintext highlighter-rouge">unsafe</code> 块保持尽可能小，风险被封装在 Reviewed 过的内部。</p>

<p>另一个高频场景是 FFI（外部函数接口），让 Rust 直接调 C 标准库：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">extern</span> <span class="s">"C"</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">abs</span><span class="p">(</span><span class="n">input</span><span class="p">:</span> <span class="nb">i32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i32</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Absolute value of -3 according to C: {}"</span><span class="p">,</span> <span class="nf">abs</span><span class="p">(</span><span class="o">-</span><span class="mi">3</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">extern</code> 块里声明的函数<strong>永远是不安全的</strong>——其他语言不遵守 Rust 的规则，安全责任全在调用者身上。反过来，<code class="language-plaintext highlighter-rouge">#[no_mangle] pub extern "C" fn</code> 也能把 Rust 函数暴露给 C（这种 extern 不需要 <code class="language-plaintext highlighter-rouge">unsafe</code>）。</p>

<h2 id="二高级-trait看懂标准库的进阶签名">二、高级 trait：看懂标准库的进阶签名</h2>

<h3 id="关联类型-vs-泛型">关联类型 vs 泛型</h3>

<p><code class="language-plaintext highlighter-rouge">Iterator</code> trait 的定义用了一个占位类型：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">trait</span> <span class="nb">Iterator</span> <span class="p">{</span>
    <span class="k">type</span> <span class="n">Item</span><span class="p">;</span>

    <span class="k">fn</span> <span class="nf">next</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">Item</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>为什么不直接写 <code class="language-plaintext highlighter-rouge">pub trait Iterator&lt;T&gt;</code>？区别在于：用泛型的话，同一个 <code class="language-plaintext highlighter-rouge">Counter</code> 可以实现 <code class="language-plaintext highlighter-rouge">Iterator&lt;u32&gt;</code>、<code class="language-plaintext highlighter-rouge">Iterator&lt;String&gt;</code> 等多个版本，每次调用 <code class="language-plaintext highlighter-rouge">next</code> 都得加类型标注来消歧义；用关联类型则<strong>一个类型只能实现一次</strong>，<code class="language-plaintext highlighter-rouge">Item</code> 选定 <code class="language-plaintext highlighter-rouge">u32</code> 后就定死，调用时无需任何标注。一句话总结：<strong>泛型是「每次使用时选类型」，关联类型是「实现时选一次」</strong>。</p>

<h3 id="默认泛型参数与运算符重载">默认泛型参数与运算符重载</h3>

<p><code class="language-plaintext highlighter-rouge">Add</code> trait 的定义是 <code class="language-plaintext highlighter-rouge">trait Add&lt;RHS=Self&gt;</code>——<code class="language-plaintext highlighter-rouge">RHS=Self</code> 就是默认类型参数，不指定时右操作数就是自身。想实现 <code class="language-plaintext highlighter-rouge">Millimeters + Meters</code>？<code class="language-plaintext highlighter-rouge">impl Add&lt;Meters&gt; for Millimeters</code> 显式覆盖默认值，单位换算在 <code class="language-plaintext highlighter-rouge">add</code> 方法里完成。这个设计还有个妙用：给现有 trait 加新的类型参数时提供默认值，可以<strong>不破坏任何已有实现</strong>地扩展 trait。</p>

<h3 id="完全限定语法">完全限定语法</h3>

<p>两个 trait 有同名方法，类型自己也有同名方法时，<code class="language-plaintext highlighter-rouge">person.fly()</code> 默认调用类型直接实现的方法；想调 trait 的用 <code class="language-plaintext highlighter-rouge">Pilot::fly(&amp;person)</code>。但遇到<strong>没有 <code class="language-plaintext highlighter-rouge">self</code> 参数的关联函数</strong>，得动用终极形态：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="o">&lt;</span><span class="n">Dog</span> <span class="k">as</span> <span class="n">Animal</span><span class="o">&gt;</span><span class="p">::</span><span class="nf">baby_name</span><span class="p">()</span>  <span class="c1">// 明确指定：Dog 的 Animal 实现里的 baby_name</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>通式是 <code class="language-plaintext highlighter-rouge">&lt;Type as Trait&gt;::function(receiver_if_method, next_arg, ...)</code>。平时用不上，报 <code class="language-plaintext highlighter-rouge">E0283: type annotations required</code> 时它就是解药。</p>

<h3 id="父-trait-与-newtype-模式">父 trait 与 newtype 模式</h3>

<p><code class="language-plaintext highlighter-rouge">trait OutlinePrint: fmt::Display</code> 声明「实现我之前必须先实现 <code class="language-plaintext highlighter-rouge">Display</code>」，于是 trait 方法内可以自由使用 <code class="language-plaintext highlighter-rouge">self.to_string()</code>。没实现 <code class="language-plaintext highlighter-rouge">Display</code> 就直接实现 <code class="language-plaintext highlighter-rouge">OutlinePrint</code>？E0277 拦下。</p>

<p>newtype 模式（用元组结构体包一层）则绕过孤儿规则——<code class="language-plaintext highlighter-rouge">Display</code> 和 <code class="language-plaintext highlighter-rouge">Vec</code> 都不归你管，但 <code class="language-plaintext highlighter-rouge">struct Wrapper(Vec&lt;String&gt;)</code> 归你管，给 <code class="language-plaintext highlighter-rouge">Wrapper</code> 实现 <code class="language-plaintext highlighter-rouge">Display</code> 即可，且<strong>零运行时开销</strong>（封装在编译期就被省略）。想透传内部类型的所有方法就实现 <code class="language-plaintext highlighter-rouge">Deref</code>，想收敛行为就手写需要的几个方法。</p>

<h2 id="三高级类型别名与动态大小">三、高级类型：<code class="language-plaintext highlighter-rouge">!</code>、别名与动态大小</h2>

<h3 id="类型别名同义词不是新类型">类型别名：同义词不是新类型</h3>

<p><code class="language-plaintext highlighter-rouge">type Kilometers = i32;</code> 里的 <code class="language-plaintext highlighter-rouge">Kilometers</code> 只是 <code class="language-plaintext highlighter-rouge">i32</code> 的<strong>另一个名字</strong>，可以互相加、可以传给收 <code class="language-plaintext highlighter-rouge">i32</code> 的函数——和 <code class="language-plaintext highlighter-rouge">Millimeters</code> 那种独立新类型完全不同（后者能拦住单位混用）。别名的主战场是<strong>消除重复</strong>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Thunk</span> <span class="o">=</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="nf">Fn</span><span class="p">()</span> <span class="o">+</span> <span class="nb">Send</span> <span class="o">+</span> <span class="k">'static</span><span class="o">&gt;</span><span class="p">;</span>  <span class="c1">// 一长串类型一个名字搞定</span>

<span class="k">type</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nn">std</span><span class="p">::</span><span class="nn">result</span><span class="p">::</span><span class="nb">Result</span><span class="o">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="n">Error</span><span class="o">&gt;</span><span class="p">;</span>  <span class="c1">// std::io 就这么干</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">std::io::Result&lt;T&gt;</code> 让 <code class="language-plaintext highlighter-rouge">Write</code> trait 里满屏的 <code class="language-plaintext highlighter-rouge">Result&lt;..., Error&gt;</code> 变成 <code class="language-plaintext highlighter-rouge">Result&lt;usize&gt;</code>，且不损失 <code class="language-plaintext highlighter-rouge">?</code> 等语法支持——因为它本质还是那个 <code class="language-plaintext highlighter-rouge">Result</code>。</p>

<h3 id="never-type从不返回的-">never type：从不返回的 <code class="language-plaintext highlighter-rouge">!</code></h3>

<p><code class="language-plaintext highlighter-rouge">!</code> 是一个没有任何值的类型，用于「发散函数」：<code class="language-plaintext highlighter-rouge">fn bar() -&gt; !</code> 表示 <code class="language-plaintext highlighter-rouge">bar</code> 永不返回。它最有意思的用法藏在 <code class="language-plaintext highlighter-rouge">continue</code> 里：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">guess</span><span class="p">:</span> <span class="nb">u32</span> <span class="o">=</span> <span class="k">match</span> <span class="n">guess</span><span class="nf">.trim</span><span class="p">()</span><span class="nf">.parse</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">Ok</span><span class="p">(</span><span class="n">num</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">num</span><span class="p">,</span>
    <span class="nf">Err</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">continue</span><span class="p">,</span>  <span class="c1">// 一个分支 u32，一个分支 continue，为什么能编译？</span>
<span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>因为 <code class="language-plaintext highlighter-rouge">continue</code> 的值就是 <code class="language-plaintext highlighter-rouge">!</code>，而 <strong><code class="language-plaintext highlighter-rouge">!</code> 可以强转为任何类型</strong>——<code class="language-plaintext highlighter-rouge">Err</code> 分支实际不会产生值（控制权交回循环），Rust 于是推断整个 match 是 <code class="language-plaintext highlighter-rouge">u32</code>。<code class="language-plaintext highlighter-rouge">panic!</code>（如 <code class="language-plaintext highlighter-rouge">Option::unwrap</code> 的 <code class="language-plaintext highlighter-rouge">None</code> 分支）和无限 <code class="language-plaintext highlighter-rouge">loop</code> 也都是 <code class="language-plaintext highlighter-rouge">!</code>。</p>

<h3 id="动态大小类型dst">动态大小类型（DST）</h3>

<p><code class="language-plaintext highlighter-rouge">str</code>（不是 <code class="language-plaintext highlighter-rouge">&amp;str</code>！）是一个 DST——长度运行时才确定，所以根本没法创建 <code class="language-plaintext highlighter-rouge">str</code> 类型的变量。解决办法你天天在用：把 DST 放到<strong>指针后面</strong>。<code class="language-plaintext highlighter-rouge">&amp;str</code> 实际是「地址 + 长度」两个值，编译期大小恒为 <code class="language-plaintext highlighter-rouge">usize</code> 的两倍。这条<strong>黄金规则</strong>还解释了 trait 对象为什么必须写成 <code class="language-plaintext highlighter-rouge">&amp;dyn Trait</code>、<code class="language-plaintext highlighter-rouge">Box&lt;dyn Trait&gt;</code>：每个 trait 也是 DST。</p>

<p>配套的 <code class="language-plaintext highlighter-rouge">Sized</code> trait 会被<strong>隐式加到每个泛型参数上</strong>（<code class="language-plaintext highlighter-rouge">fn generic&lt;T&gt;(t: T)</code> 实为 <code class="language-plaintext highlighter-rouge">fn generic&lt;T: Sized&gt;</code>）；用 <code class="language-plaintext highlighter-rouge">T: ?Sized</code>（只能用于 <code class="language-plaintext highlighter-rouge">Sized</code>）可放宽，此时参数要改成 <code class="language-plaintext highlighter-rouge">&amp;T</code> 这类指针形式。</p>

<h2 id="四高级函数与闭包">四、高级函数与闭包</h2>

<p>函数可以当参数传——类型是 <strong><code class="language-plaintext highlighter-rouge">fn</code>（小写，函数指针）</strong>，注意不是闭包 trait <code class="language-plaintext highlighter-rouge">Fn</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">do_twice</span><span class="p">(</span><span class="n">f</span><span class="p">:</span> <span class="k">fn</span><span class="p">(</span><span class="nb">i32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i32</span><span class="p">,</span> <span class="n">arg</span><span class="p">:</span> <span class="nb">i32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i32</span> <span class="p">{</span>
    <span class="nf">f</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span> <span class="o">+</span> <span class="nf">f</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span>
<span class="p">}</span>

<span class="nf">do_twice</span><span class="p">(</span><span class="n">add_one</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>  <span class="c1">// 12</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>函数指针实现了全部三个闭包 trait，所以收闭包的函数也能收函数指针；反之，<strong>只收 <code class="language-plaintext highlighter-rouge">fn</code> 不收闭包</strong>的场景主要是与 C 代码交互——C 没有闭包。另外 <code class="language-plaintext highlighter-rouge">map(ToString::to_string)</code>、<code class="language-plaintext highlighter-rouge">map(Status::Value)</code>（元组结构体构造器也是函数指针！）这类写法之所以可行，靠的就是函数指针与完全限定语法。</p>

<p>返回闭包则不能直接写返回类型（闭包是 trait，大小未知），标准姿势是 trait 对象：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">returns_closure</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="nf">Fn</span><span class="p">(</span><span class="nb">i32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i32</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(|</span><span class="n">x</span><span class="p">|</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="五宏为写代码而写代码">五、宏：为写代码而写代码</h2>

<p>宏 = 元编程，与函数的关键区别一张表看清：</p>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>函数</th>
      <th>宏</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>参数数量/类型</td>
      <td>必须声明并检查</td>
      <td>可接受任意数量（<code class="language-plaintext highlighter-rouge">println!</code>、<code class="language-plaintext highlighter-rouge">vec!</code>）</td>
    </tr>
    <tr>
      <td>展开时机</td>
      <td>运行时调用</td>
      <td><strong>编译前展开</strong>，可在类型上实现 trait</td>
    </tr>
    <tr>
      <td>可读性/维护成本</td>
      <td>较低</td>
      <td>更复杂（写生成代码的代码）</td>
    </tr>
    <tr>
      <td>定义/调用顺序</td>
      <td>任意</td>
      <td>调用前必须先定义或引入作用域</td>
    </tr>
  </tbody>
</table>

<p>声明宏 <code class="language-plaintext highlighter-rouge">macro_rules!</code> 本质是「对 Rust 代码做 <code class="language-plaintext highlighter-rouge">match</code>」——匹配的是<strong>代码结构</strong>而非值。<code class="language-plaintext highlighter-rouge">vec!</code> 的简化版一窥究竟：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nd">#[macro_export]</span>
<span class="nd">macro_rules!</span> <span class="n">vec</span> <span class="p">{</span>
    <span class="p">(</span> <span class="nv">$</span><span class="p">(</span> <span class="nv">$x:expr</span> <span class="p">),</span><span class="o">*</span> <span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
        <span class="p">{</span>
            <span class="k">let</span> <span class="k">mut</span> <span class="n">temp_vec</span> <span class="o">=</span> <span class="nn">Vec</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
            <span class="nv">$</span><span class="p">(</span> <span class="n">temp_vec</span><span class="nf">.push</span><span class="p">(</span><span class="nv">$x</span><span class="p">);</span> <span class="p">)</span><span class="o">*</span>
            <span class="n">temp_vec</span>
        <span class="p">}</span>
    <span class="p">};</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">$x:expr</code> 捕获任意表达式，<code class="language-plaintext highlighter-rouge">$()*</code> 按匹配次数重复生成代码。<code class="language-plaintext highlighter-rouge">vec![1, 2, 3]</code> 展开后就是三行 <code class="language-plaintext highlighter-rouge">push</code>。</p>

<p>过程宏更像函数：吃进 <code class="language-plaintext highlighter-rouge">TokenStream</code>（token 序列），吐出 <code class="language-plaintext highlighter-rouge">TokenStream</code>，必须放在独立的 <code class="language-plaintext highlighter-rouge">proc-macro = true</code> crate 里。三种形态：</p>

<table>
  <thead>
    <tr>
      <th>类型</th>
      <th>触发方式</th>
      <th>例子</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>自定义 derive</td>
      <td><code class="language-plaintext highlighter-rouge">#[derive(HelloMacro)]</code></td>
      <td>自动生成 trait 实现</td>
    </tr>
    <tr>
      <td>类属性宏</td>
      <td><code class="language-plaintext highlighter-rouge">#[route(GET, "/")]</code></td>
      <td>Web 框架的路由标注（可用于函数）</td>
    </tr>
    <tr>
      <td>类函数宏</td>
      <td><code class="language-plaintext highlighter-rouge">sql!(SELECT * FROM posts ...)</code></td>
      <td>解析并校验 SQL 语法</td>
    </tr>
  </tbody>
</table>

<p>derive 宏的标准流水线：<code class="language-plaintext highlighter-rouge">syn</code> 把 <code class="language-plaintext highlighter-rouge">TokenStream</code> 解析成语法树（拿到 <code class="language-plaintext highlighter-rouge">ast.ident</code> 即结构体名）→ <code class="language-plaintext highlighter-rouge">quote!</code> 用模板（<code class="language-plaintext highlighter-rouge">#name</code> 占位符）生成 <code class="language-plaintext highlighter-rouge">impl HelloMacro for #name</code> 代码 → <code class="language-plaintext highlighter-rouge">into()</code> 转回 <code class="language-plaintext highlighter-rouge">TokenStream</code> 交给编译器。<code class="language-plaintext highlighter-rouge">#[derive(HelloMacro)] struct Pancakes;</code> 就能自动打印 <code class="language-plaintext highlighter-rouge">Hello, Macro! My name is Pancakes!</code>——这正是 Rust 没有反射却能做到「运行时打印类型名」的方式。</p>

<h2 id="六实践建议与总结">六、实践建议与总结</h2>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">unsafe</code> 的三条纪律</strong>：块保持尽可能小；优先封装成安全 API 对外暴露；可变静态变量能不用就不用，用第16章的并发原语替代。</li>
  <li><strong>区分「新类型」和「别名」</strong>：要类型安全拦住单位混用，用 newtype；只为少打字，用 <code class="language-plaintext highlighter-rouge">type</code> 别名。</li>
  <li><strong>见到看不懂的签名别慌</strong>：<code class="language-plaintext highlighter-rouge">T: ?Sized</code>、<code class="language-plaintext highlighter-rouge">Add&lt;RHS=Self&gt;</code>、<code class="language-plaintext highlighter-rouge">&lt;Dog as Animal&gt;::fn</code>、<code class="language-plaintext highlighter-rouge">-&gt; Box&lt;dyn Fn&gt;</code> 都出自本章——把它当参考手册，下次在报错信息或他人代码里碰到，回来翻即可。</li>
</ol>

<p><strong>总结</strong>：本章把 Rust 的「地下室」逛了一遍——<code class="language-plaintext highlighter-rouge">unsafe</code> 撤掉护栏换取与硬件和 C 交互的能力；关联类型、默认泛型参数、完全限定语法、父 trait 和 newtype 模式撑起了标准库的进阶签名；<code class="language-plaintext highlighter-rouge">!</code> 与 DST 揭示了类型系统的两个隐藏角落；宏则让 Rust 拥有了编译期生成代码的元编程能力。下一章是最后一站：把全书所学倾注到一个多线程 Web 服务器项目里。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="rust" /><category term="unsafe" /><category term="traits" /><category term="macros" /><category term="advanced" /><summary type="html"><![CDATA[开篇：Rust 的「地下室工具房」]]></summary></entry><entry><title type="html">Rust 学习笔记（19/21）：模式和匹配——你天天在用，却没发现它无处不在</title><link href="https://0end1.github.io/2026/09/19/rust-patterns-and-matching/" rel="alternate" type="text/html" title="Rust 学习笔记（19/21）：模式和匹配——你天天在用，却没发现它无处不在" /><published>2026-09-19T09:00:00+08:00</published><updated>2026-09-19T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/19/rust-patterns-and-matching</id><content type="html" xml:base="https://0end1.github.io/2026/09/19/rust-patterns-and-matching/"><![CDATA[<h2 id="开篇let-x--5-里藏着一个秘密">开篇：<code class="language-plaintext highlighter-rouge">let x = 5;</code> 里藏着一个秘密</h2>

<p>先看一行你写过不下百次的代码：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>问你这里用了什么语法特性？「变量绑定」，对吧。但书里揭了个底：<strong><code class="language-plaintext highlighter-rouge">x</code> 本身就是一个模式</strong>。<code class="language-plaintext highlighter-rouge">let</code> 语句的完整形态其实是 <code class="language-plaintext highlighter-rouge">let PATTERN = EXPRESSION;</code>——变量名只是「形式最朴素的模式」，它的含义是「把任何值绑定到变量 <code class="language-plaintext highlighter-rouge">x</code>，不管值是什么」。</p>

<p>这就像收快递：快递单上的收件格式（模式）和实际包裹（值）对上了，你就能「拆开取件」——把里面的东西一件件拿出来用。Rust 里拆这个「包裹」的动作无处不在，<code class="language-plaintext highlighter-rouge">match</code>、<code class="language-plaintext highlighter-rouge">if let</code>、<code class="language-plaintext highlighter-rouge">for</code>、<code class="language-plaintext highlighter-rouge">let</code>、甚至函数参数，全都是同一个机制的不同入口。本章就把「包裹拆解说明书」完整过一遍：模式能用在哪、什么模式必须百发百中、以及九种模式语法的正确用法。</p>

<h2 id="一模式的六个使用位置">一、模式的六个使用位置</h2>

<p>你已经在不经意间用过很多模式了。先来一张全局地图：</p>

<table>
  <thead>
    <tr>
      <th>使用位置</th>
      <th>接受的模式类型</th>
      <th>典型场景</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">match</code> 分支</td>
      <td>前面的分支可反驳 + 最后兜底不可反驳</td>
      <td>穷尽地分派所有情况</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">if let</code> / <code class="language-plaintext highlighter-rouge">else if let</code></td>
      <td>只接受<strong>可反驳</strong>模式</td>
      <td>只关心某一种情况时的简写</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">while let</code></td>
      <td>只接受<strong>可反驳</strong>模式</td>
      <td>「取到就继续」的循环，如弹栈</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">for</code> 循环</td>
      <td>只接受<strong>不可反驳</strong>模式</td>
      <td>遍历时解构，如 <code class="language-plaintext highlighter-rouge">(index, value)</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let</code> 语句</td>
      <td>只接受<strong>不可反驳</strong>模式</td>
      <td>解构元组一次创建多个变量</td>
    </tr>
    <tr>
      <td>函数参数</td>
      <td>只接受<strong>不可反驳</strong>模式</td>
      <td>参数位置直接解构</td>
    </tr>
  </tbody>
</table>

<p>两个容易忽略的位置：</p>

<p><strong><code class="language-plaintext highlighter-rouge">let</code> 解构元组</strong>。<code class="language-plaintext highlighter-rouge">let (x, y, z) = (1, 2, 3);</code> 会把 <code class="language-plaintext highlighter-rouge">1</code>、<code class="language-plaintext highlighter-rouge">2</code>、<code class="language-plaintext highlighter-rouge">3</code> 分别绑定到三个变量。但元素数量必须严格对上，否则直接编译错误：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
<span class="c1">// error[E0308]: mismatched types</span>
<span class="c1">// expected a tuple with 3 elements, found one with 2 elements</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">while let</code> 弹栈</strong>是它最经典的用武之地——<code class="language-plaintext highlighter-rouge">stack.pop()</code> 返回 <code class="language-plaintext highlighter-rouge">Option</code>，取到 <code class="language-plaintext highlighter-rouge">Some</code> 就循环，取到 <code class="language-plaintext highlighter-rouge">None</code> 就自然停止：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="k">mut</span> <span class="n">stack</span> <span class="o">=</span> <span class="nn">Vec</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="n">stack</span><span class="nf">.push</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="n">stack</span><span class="nf">.push</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
<span class="n">stack</span><span class="nf">.push</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>

<span class="k">while</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">top</span><span class="p">)</span> <span class="o">=</span> <span class="n">stack</span><span class="nf">.pop</span><span class="p">()</span> <span class="p">{</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">top</span><span class="p">);</span> <span class="c1">// 依次打印 3、2、1</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>函数参数</strong>也能是模式，这是最少人知道的一个：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">print_coordinates</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span> <span class="o">&amp;</span><span class="p">(</span><span class="nb">i32</span><span class="p">,</span> <span class="nb">i32</span><span class="p">))</span> <span class="p">{</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"Current location: ({}, {})"</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>值 <code class="language-plaintext highlighter-rouge">&amp;(3, 5)</code> 匹配模式 <code class="language-plaintext highlighter-rouge">&amp;(x, y)</code>，<code class="language-plaintext highlighter-rouge">x</code> 得到 <code class="language-plaintext highlighter-rouge">3</code>，<code class="language-plaintext highlighter-rouge">y</code> 得到 <code class="language-plaintext highlighter-rouge">5</code>。闭包参数同理。</p>

<h2 id="二可反驳性模式会不会失手">二、可反驳性：模式会不会「失手」</h2>

<p>注意上面表格里反复出现的一对词。模式分两种：</p>

<ul>
  <li><strong>不可反驳（irrefutable）</strong>：能匹配任何传进来的值，永远不会失败。<code class="language-plaintext highlighter-rouge">let x = 5;</code> 里的 <code class="language-plaintext highlighter-rouge">x</code> 就是——任何值它都接得住。</li>
  <li><strong>可反驳（refutable）</strong>：对某些可能的值会匹配失败。<code class="language-plaintext highlighter-rouge">if let Some(x) = a_value</code> 里的 <code class="language-plaintext highlighter-rouge">Some(x)</code> 就是——如果 <code class="language-plaintext highlighter-rouge">a_value</code> 是 <code class="language-plaintext highlighter-rouge">None</code>，匹配就失败了。</li>
</ul>

<p>这条区分决定了各位置「选人标准」，也是新手最常撞上的编译错误来源：</p>

<table>
  <thead>
    <tr>
      <th>位置</th>
      <th>为什么</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let</code> / <code class="language-plaintext highlighter-rouge">for</code> / 函数参数</td>
      <td>只接受不可反驳模式——匹配不上就没有有意义的后续可做</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">if let</code> / <code class="language-plaintext highlighter-rouge">while let</code></td>
      <td>只接受可反驳模式——它们的存在意义就是「处理可能失败」</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">match</code> 分支</td>
      <td>前面的分支用可反驳模式，最后一个兜底分支用不可反驳模式</td>
    </tr>
  </tbody>
</table>

<p>两个方向的反例都值得看一眼。在 <code class="language-plaintext highlighter-rouge">let</code> 里用可反驳模式：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">=</span> <span class="n">some_option_value</span><span class="p">;</span>
<span class="c1">// error[E0005]: refutable pattern in local binding: `None` not covered</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>编译器的态度很明确：万一进来的是 <code class="language-plaintext highlighter-rouge">None</code>，这行代码没法有意义地继续，所以不放行。修复方式是改用 <code class="language-plaintext highlighter-rouge">if let</code>，给代码一个「匹配不上就跳过」的出路。</p>

<p>反过来，在 <code class="language-plaintext highlighter-rouge">if let</code> 里用不可反驳模式也不行——它会<strong>警告</strong>（不是报错）<code class="language-plaintext highlighter-rouge">irrefutable if-let pattern</code>：一个永远匹配的条件写在 <code class="language-plaintext highlighter-rouge">if let</code> 里毫无意义，不如直接写 <code class="language-plaintext highlighter-rouge">let</code>。</p>

<h2 id="三模式语法工具箱">三、模式语法工具箱</h2>

<h3 id="基础四式字面量命名变量或范围">基础四式：字面量、命名变量、或、范围</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">match</span> <span class="n">x</span> <span class="p">{</span>
    <span class="mi">1</span> <span class="p">|</span> <span class="mi">2</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"one or two"</span><span class="p">),</span>   <span class="c1">// | 表示「或」</span>
    <span class="mi">1</span><span class="o">..=</span><span class="mi">5</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"one through five"</span><span class="p">),</span> <span class="c1">// 闭区间范围，比 1|2|3|4|5 省事得多</span>
    <span class="n">_</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"anything"</span><span class="p">),</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>范围 <code class="language-plaintext highlighter-rouge">..=</code> 只允许用于数字和 <code class="language-plaintext highlighter-rouge">char</code>，因为只有这两种类型编译器能在编译期判断范围是否为空。</p>

<h3 id="命名变量的遮蔽陷阱">命名变量的「遮蔽」陷阱</h3>

<p>这是本章最值得多看两眼的地方。猜猜下面的代码打印什么？</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">x</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>
<span class="k">let</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>

<span class="k">match</span> <span class="n">x</span> <span class="p">{</span>
    <span class="nf">Some</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"Got 50"</span><span class="p">),</span>
    <span class="nf">Some</span><span class="p">(</span><span class="n">y</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"Matched, y = {:?}"</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span>
    <span class="n">_</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"Default case, x = {:?}"</span><span class="p">,</span> <span class="n">x</span><span class="p">),</span>
<span class="p">}</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"at the end: x = {:?}, y = {:?}"</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>答案是 <code class="language-plaintext highlighter-rouge">Matched, y = 5</code>——<strong>不是</strong> <code class="language-plaintext highlighter-rouge">10</code>。<code class="language-plaintext highlighter-rouge">match</code> 会开启新作用域，第二个分支的 <code class="language-plaintext highlighter-rouge">Some(y)</code> 引入了一个<strong>新的</strong>变量 <code class="language-plaintext highlighter-rouge">y</code>（遮蔽外部那个 <code class="language-plaintext highlighter-rouge">10</code>），它匹配任何 <code class="language-plaintext highlighter-rouge">Some</code> 里的值，于是绑定了 <code class="language-plaintext highlighter-rouge">x</code> 里的 <code class="language-plaintext highlighter-rouge">5</code>。循环结束后，外部的 <code class="language-plaintext highlighter-rouge">y</code> 依然是 <code class="language-plaintext highlighter-rouge">10</code>。</p>

<p>如果你的本意是「比较 <code class="language-plaintext highlighter-rouge">Some</code> 里的值和外部 <code class="language-plaintext highlighter-rouge">y</code> 是否相等」，答案是<strong>匹配守卫</strong>（后面讲到）：把模式改名为 <code class="language-plaintext highlighter-rouge">Some(n)</code>，再写 <code class="language-plaintext highlighter-rouge">Some(n) if n == y</code>。</p>

<h3 id="解构拆结构体枚举和嵌套">解构：拆结构体、枚举和嵌套</h3>

<p>解构结构体时，变量名不必与字段名一致（<code class="language-plaintext highlighter-rouge">let Point { x: a, y: b } = p;</code>），但更常用的是字段简写 <code class="language-plaintext highlighter-rouge">let Point { x, y } = p;</code>。还可以把<strong>字面量混进模式</strong>，用来「测一部分、绑另一部分」：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">match</span> <span class="n">p</span> <span class="p">{</span>
    <span class="n">Point</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="mi">0</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"On the x axis at {}"</span><span class="p">,</span> <span class="n">x</span><span class="p">),</span>
    <span class="n">Point</span> <span class="p">{</span> <span class="n">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">y</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"On the y axis at {}"</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span>
    <span class="n">Point</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"On neither axis: ({}, {})"</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>解构枚举的模式必须对应枚举定义数据的方式：无数据的 <code class="language-plaintext highlighter-rouge">Quit</code> 只能匹配字面量；类结构体成员 <code class="language-plaintext highlighter-rouge">Move { x, y }</code> 像结构体；类元组成员 <code class="language-plaintext highlighter-rouge">Write(text)</code>、<code class="language-plaintext highlighter-rouge">ChangeColor(r, g, b)</code> 像元组。而且<strong>嵌套随便拆</strong>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">match</span> <span class="n">msg</span> <span class="p">{</span>
    <span class="nn">Message</span><span class="p">::</span><span class="nf">ChangeColor</span><span class="p">(</span><span class="nn">Color</span><span class="p">::</span><span class="nf">Rgb</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">b</span><span class="p">))</span> <span class="k">=&gt;</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
    <span class="nn">Message</span><span class="p">::</span><span class="nf">ChangeColor</span><span class="p">(</span><span class="nn">Color</span><span class="p">::</span><span class="nf">Hsv</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">s</span><span class="p">,</span> <span class="n">v</span><span class="p">))</span> <span class="k">=&gt;</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
    <span class="n">_</span> <span class="k">=&gt;</span> <span class="p">()</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>两个枚举的嵌套，一个模式里一次拆穿。结构体和元组还能再混搭：<code class="language-plaintext highlighter-rouge">let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });</code>。</p>

<h3 id="忽略值的四种姿势">忽略值的四种姿势</h3>

<table>
  <thead>
    <tr>
      <th>写法</th>
      <th>效果</th>
      <th>注意</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">_</code></td>
      <td>匹配但不绑定任何值</td>
      <td>不会移动所有权</td>
    </tr>
    <tr>
      <td>模式内嵌 <code class="language-plaintext highlighter-rouge">_</code></td>
      <td>忽略部分值，如 <code class="language-plaintext highlighter-rouge">(first, _, third, _, fifth)</code></td>
      <td>只测形状不看内容</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">_x</code> 前缀</td>
      <td><strong>仍然绑定</strong>，只是压制未使用警告</td>
      <td>会移动所有权！</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">..</code></td>
      <td>忽略剩余所有部分</td>
      <td>每个模式只能用一次</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">_</code> 和 <code class="language-plaintext highlighter-rouge">_x</code> 的区别很微妙但重要：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">s</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"Hello!"</span><span class="p">));</span>

<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">_s</span><span class="p">)</span> <span class="o">=</span> <span class="n">s</span> <span class="p">{</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"found a string"</span><span class="p">);</span> <span class="p">}</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}"</span><span class="p">,</span> <span class="n">s</span><span class="p">);</span> <span class="c1">// 错误！s 已被移动进 _s</span>

<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">s</span> <span class="p">{</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"found a string"</span><span class="p">);</span> <span class="p">}</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}"</span><span class="p">,</span> <span class="n">s</span><span class="p">);</span> <span class="c1">// 正常。_ 不绑定值，s 没有被移动</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">..</code> 的使用必须无歧义，<code class="language-plaintext highlighter-rouge">(first, .., last)</code> 合法，但 <code class="language-plaintext highlighter-rouge">(.., second, ..)</code> 会报错 <code class="language-plaintext highlighter-rouge">`..` can only be used once per tuple or tuple struct pattern</code>——编译器无法确定 <code class="language-plaintext highlighter-rouge">second</code> 到底是哪个位置。</p>

<h3 id="匹配守卫给模式追加-if-条件">匹配守卫：给模式追加 if 条件</h3>

<p>模式表达不了「<code class="language-plaintext highlighter-rouge">Some</code> 里的值小于 5」这种条件，匹配守卫可以：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">num</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>

<span class="k">match</span> <span class="n">num</span> <span class="p">{</span>
    <span class="nf">Some</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="k">if</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="mi">5</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"less than five: {}"</span><span class="p">,</span> <span class="n">x</span><span class="p">),</span>
    <span class="nf">Some</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">x</span><span class="p">),</span>
    <span class="nb">None</span> <span class="k">=&gt;</span> <span class="p">(),</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>守卫的条件可以使用模式中创建的变量（甚至外部的变量），这也是修复上面遮蔽陷阱的标准姿势：<code class="language-plaintext highlighter-rouge">Some(n) if n == y</code> 里的 <code class="language-plaintext highlighter-rouge">y</code> 是外部的 <code class="language-plaintext highlighter-rouge">y</code>，因为 <code class="language-plaintext highlighter-rouge">if n == y</code> 不是模式、不引入新变量。</p>

<p>一个优先级细节：<code class="language-plaintext highlighter-rouge">4 | 5 | 6 if y</code> 的语义是 <strong><code class="language-plaintext highlighter-rouge">(4 | 5 | 6) if y</code></strong>——守卫作用于整个「或」组合，而不只是最后一个 <code class="language-plaintext highlighter-rouge">6</code>。</p>

<h3 id="-绑定一边测试一边抓住">@ 绑定：一边测试一边抓住</h3>

<p>最后一件法宝。想测「id 在 3 到 7 之间」，又想在分支里用上这个值？<code class="language-plaintext highlighter-rouge">@</code> 运算符两件事一起干：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">match</span> <span class="n">msg</span> <span class="p">{</span>
    <span class="nn">Message</span><span class="p">::</span><span class="n">Hello</span> <span class="p">{</span> <span class="n">id</span><span class="p">:</span> <span class="n">id_variable</span> <span class="o">@</span> <span class="mi">3</span><span class="o">..=</span><span class="mi">7</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Found an id in range: {}"</span><span class="p">,</span> <span class="n">id_variable</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nn">Message</span><span class="p">::</span><span class="n">Hello</span> <span class="p">{</span> <span class="n">id</span><span class="p">:</span> <span class="mi">10</span><span class="o">..=</span><span class="mi">12</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Found an id in another range"</span><span class="p">)</span> <span class="c1">// 测了范围但拿不到值</span>
    <span class="p">}</span>
    <span class="nn">Message</span><span class="p">::</span><span class="n">Hello</span> <span class="p">{</span> <span class="n">id</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Found some other id: {}"</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span> <span class="c1">// 拿得到值但没测范围</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">id_variable @ 3..=7</code> = 「测试它在这个范围 + 把它存进变量」。中间分支只测不存，最后分支只存不测——<code class="language-plaintext highlighter-rouge">@</code> 让你两个都要。</p>

<h2 id="四实践建议与总结">四、实践建议与总结</h2>

<ol>
  <li><strong>记住「选人标准」</strong>：<code class="language-plaintext highlighter-rouge">let</code>/<code class="language-plaintext highlighter-rouge">for</code>/函数参数要百发百中（不可反驳），<code class="language-plaintext highlighter-rouge">if let</code>/<code class="language-plaintext highlighter-rouge">while let</code> 专为「可能失手」而生。看到 E0005 报错，第一反应是「这该用 <code class="language-plaintext highlighter-rouge">if let</code> 而不是 <code class="language-plaintext highlighter-rouge">let</code>」。</li>
  <li><strong>警惕 <code class="language-plaintext highlighter-rouge">match</code> 里的变量遮蔽</strong>：分支模式里的名字是新变量，不是外部同名变量。想比较外部值，用匹配守卫 <code class="language-plaintext highlighter-rouge">Some(n) if n == y</code>。</li>
  <li><strong>忽略值想清楚要不要「接住」</strong>：<code class="language-plaintext highlighter-rouge">_</code> 不绑定不移动所有权，<code class="language-plaintext highlighter-rouge">_x</code> 会真的把值拿走。碰到所有权被意外移走的编译错误，检查是不是用了 <code class="language-plaintext highlighter-rouge">_x</code>。</li>
</ol>

<p><strong>总结</strong>：模式是 Rust 中区分数据形状的通用语法，<code class="language-plaintext highlighter-rouge">match</code> 靠它实现穷尽性检查，<code class="language-plaintext highlighter-rouge">let</code> 和函数参数靠它实现优雅解构。九种语法——字面量、命名变量、<code class="language-plaintext highlighter-rouge">|</code>、<code class="language-plaintext highlighter-rouge">..=</code>、解构、<code class="language-plaintext highlighter-rouge">_</code>/<code class="language-plaintext highlighter-rouge">_x</code>/<code class="language-plaintext highlighter-rouge">..</code>、匹配守卫、<code class="language-plaintext highlighter-rouge">@</code> 绑定——组合起来能写出既简洁又安全的分派逻辑。下一章我们进入「高级特征」：unsafe、trait 高阶玩法等特定场景武器。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="rust" /><category term="pattern-matching" /><category term="match" /><category term="refutability" /><category term="destructuring" /><summary type="html"><![CDATA[开篇：let x = 5; 里藏着一个秘密]]></summary></entry><entry><title type="html">Rust 学习笔记（18/21）：Rust 的面向对象特性——trait 对象与状态模式</title><link href="https://0end1.github.io/2026/09/18/rust-oop-features/" rel="alternate" type="text/html" title="Rust 学习笔记（18/21）：Rust 的面向对象特性——trait 对象与状态模式" /><published>2026-09-18T09:00:00+08:00</published><updated>2026-09-18T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/18/rust-oop-features</id><content type="html" xml:base="https://0end1.github.io/2026/09/18/rust-oop-features/"><![CDATA[<blockquote>
  <p>对应书源：《Rust 程序设计语言》第 17 章「Rust 的面向对象特性」</p>
</blockquote>

<h2 id="开篇rust-到底算不算面向对象语言">开篇：「Rust 到底算不算面向对象语言？」</h2>

<p>番茄是水果还是蔬菜？植物学说它是水果，厨房里说它是蔬菜——答案取决于你采用哪套标准。「Rust 是不是面向对象」也是同一类问题，编程社区至今没有统一定义，在某些定义下 Rust 是，在另一些定义下不是。</p>

<p>所以本章不急着下结论，而是把大家普遍认可的面向对象特征拆成三块——<strong>对象、封装、继承</strong>——逐一对照 Rust 的实际做法。接着再动手实现一个经典设计模式（状态模式），最后讨论一个关键问题：照搬面向对象模式，是不是发挥了 Rust 的全部优势？</p>

<h2 id="一对象包含数据和行为这一条-rust-达标">一、对象包含数据和行为：这一条 Rust 达标</h2>

<p>《设计模式》（GoF，”四人帮”）对面向对象编程的定义是：</p>

<blockquote>
  <p>面向对象的程序由对象组成。一个对象包含数据和操作这些数据的过程，这些过程通常被称为方法或操作。</p>
</blockquote>

<p>按这个标准，Rust 完全达标：<code class="language-plaintext highlighter-rouge">struct</code> 和 <code class="language-plaintext highlighter-rouge">enum</code> 装数据，<code class="language-plaintext highlighter-rouge">impl</code> 块提供方法。唯一的差别在于，Rust 刻意<strong>不</strong>把结构体和枚举叫作「对象」，以便和其他语言里的对象概念区分开。功能上，它们提供的是同一件事。</p>

<h2 id="二封装pub-就是那道门">二、封装：<code class="language-plaintext highlighter-rouge">pub</code> 就是那道门</h2>

<p>封装的意思是：对象的实现细节对使用它的代码不可见，唯一交互方式是公有 API。这样重构内部时不必改动外部代码。</p>

<p>Rust 的做法你已经熟悉——默认私有，用 <code class="language-plaintext highlighter-rouge">pub</code> 逐项开放。书里给了个漂亮的例子 <code class="language-plaintext highlighter-rouge">AveragedCollection</code>，它维护一个整型列表与列表平均值，并把平均值缓存下来：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">struct</span> <span class="n">AveragedCollection</span> <span class="p">{</span>
    <span class="n">list</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">i32</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="n">average</span><span class="p">:</span> <span class="nb">f64</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">AveragedCollection</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">add</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">i32</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.list</span><span class="nf">.push</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
        <span class="k">self</span><span class="nf">.update_average</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">remove</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">i32</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">result</span> <span class="o">=</span> <span class="k">self</span><span class="py">.list</span><span class="nf">.pop</span><span class="p">();</span>
        <span class="k">match</span> <span class="n">result</span> <span class="p">{</span>
            <span class="nf">Some</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="k">self</span><span class="nf">.update_average</span><span class="p">();</span>
                <span class="nf">Some</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="nb">None</span> <span class="k">=&gt;</span> <span class="nb">None</span><span class="p">,</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">average</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">f64</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.average</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">update_average</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">total</span><span class="p">:</span> <span class="nb">i32</span> <span class="o">=</span> <span class="k">self</span><span class="py">.list</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.sum</span><span class="p">();</span>
        <span class="k">self</span><span class="py">.average</span> <span class="o">=</span> <span class="n">total</span> <span class="k">as</span> <span class="nb">f64</span> <span class="o">/</span> <span class="k">self</span><span class="py">.list</span><span class="nf">.len</span><span class="p">()</span> <span class="k">as</span> <span class="nb">f64</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>结构体本身是 <code class="language-plaintext highlighter-rouge">pub</code>，但 <code class="language-plaintext highlighter-rouge">list</code> 和 <code class="language-plaintext highlighter-rouge">average</code> 字段仍是私有的。这一点至关重要：外部代码无法直接增删 <code class="language-plaintext highlighter-rouge">list</code>，因此「平均值与列表不同步」这种 bug 在结构上就不可能发生。而好处随之而来——将来想把 <code class="language-plaintext highlighter-rouge">Vec&lt;i32&gt;</code> 换成 <code class="language-plaintext highlighter-rouge">HashSet&lt;i32&gt;</code>，只要 <code class="language-plaintext highlighter-rouge">add</code>/<code class="language-plaintext highlighter-rouge">remove</code>/<code class="language-plaintext highlighter-rouge">average</code> 的签名不变，调用方一行都不用改；反之如果把 <code class="language-plaintext highlighter-rouge">list</code> 设为 <code class="language-plaintext highlighter-rouge">pub</code>，外部直接操作它，换数据结构就会引发连锁修改。</p>

<h2 id="三继承rust-明确没有但给了两条替代路径">三、继承：Rust 明确没有，但给了两条替代路径</h2>

<p>如果「必须有继承才算面向对象」，那 Rust 就不算。你无法定义一个结构体继承另一个结构体的字段和方法。但人们用继承通常出于两个原因，Rust 各有对应方案：</p>

<table>
  <thead>
    <tr>
      <th>使用继承的原因</th>
      <th>Rust 的替代方案</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>代码复用</strong>：一个类型实现的行为，想给另一个类型直接用</td>
      <td>trait 的默认方法实现（如 <code class="language-plaintext highlighter-rouge">Summary</code> 的 <code class="language-plaintext highlighter-rouge">summarize</code>），实现该 trait 即可拥有，也可覆盖</td>
    </tr>
    <tr>
      <td><strong>多态</strong>：子类型可以用在父类型出现的地方</td>
      <td>泛型 + trait bound（有界参数化多态），或 trait 对象</td>
    </tr>
  </tbody>
</table>

<p>近年来继承在很多语言里「失宠」，因为它常常强迫子类共享并不需要的父类特性，设计变僵硬，还可能出现「子类调用了其实不适用于它的方法」。有些语言还只允许单继承，进一步限制了灵活性。出于这些原因，Rust 选了另一条路：<strong>用 trait 对象实现多态</strong>。</p>

<h2 id="四trait-对象让不同类型的值共处一个集合">四、trait 对象：让不同类型的值共处一个集合</h2>

<p>第 8 章说过，<code class="language-plaintext highlighter-rouge">Vec</code> 只能存同种元素，用 <code class="language-plaintext highlighter-rouge">SpreadsheetCell</code> 枚举可以绕开——但那要求类型集合在编译期就固定。如果写库的人无法预知使用者会新增什么类型呢？</p>

<p>设想一个 GUI 库：它遍历组件列表，对每个组件调用 <code class="language-plaintext highlighter-rouge">draw</code>。库作者知道 <code class="language-plaintext highlighter-rouge">Button</code>、<code class="language-plaintext highlighter-rouge">TextField</code>，但使用者可能想加 <code class="language-plaintext highlighter-rouge">Image</code>、<code class="language-plaintext highlighter-rouge">SelectBox</code>。做法是定义一个 <code class="language-plaintext highlighter-rouge">Draw</code> trait，让 <code class="language-plaintext highlighter-rouge">Screen</code> 持有 trait 对象的 vector：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">trait</span> <span class="n">Draw</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">draw</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">struct</span> <span class="n">Screen</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">components</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">Draw</span><span class="o">&gt;&gt;</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Screen</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">run</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">component</span> <span class="k">in</span> <span class="k">self</span><span class="py">.components</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
            <span class="n">component</span><span class="nf">.draw</span><span class="p">();</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Box&lt;dyn Draw&gt;</code> 就是 trait 对象：它是「任何实现了 <code class="language-plaintext highlighter-rouge">Draw</code> 的类型」的替身。使用者只需为自己的类型实现 <code class="language-plaintext highlighter-rouge">Draw</code>，就能塞进 <code class="language-plaintext highlighter-rouge">Screen</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">gui</span><span class="p">::{</span><span class="n">Screen</span><span class="p">,</span> <span class="n">Button</span><span class="p">};</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">screen</span> <span class="o">=</span> <span class="n">Screen</span> <span class="p">{</span>
        <span class="n">components</span><span class="p">:</span> <span class="nd">vec!</span><span class="p">[</span>
            <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">SelectBox</span> <span class="p">{</span>
                <span class="n">width</span><span class="p">:</span> <span class="mi">75</span><span class="p">,</span>
                <span class="n">height</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
                <span class="n">options</span><span class="p">:</span> <span class="nd">vec!</span><span class="p">[</span>
                    <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"Yes"</span><span class="p">),</span>
                    <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"Maybe"</span><span class="p">),</span>
                    <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"No"</span><span class="p">),</span>
                <span class="p">],</span>
            <span class="p">}),</span>
            <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">Button</span> <span class="p">{</span>
                <span class="n">width</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
                <span class="n">height</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
                <span class="n">label</span><span class="p">:</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"OK"</span><span class="p">),</span>
            <span class="p">}),</span>
        <span class="p">],</span>
    <span class="p">};</span>

    <span class="n">screen</span><span class="nf">.run</span><span class="p">();</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如果换成泛型写法 <code class="language-plaintext highlighter-rouge">Screen&lt;T: Draw&gt;</code>，<code class="language-plaintext highlighter-rouge">components</code> 就只能是<strong>清一色</strong> <code class="language-plaintext highlighter-rouge">Button</code> 或清一色 <code class="language-plaintext highlighter-rouge">TextField</code>。两种写法该怎么选？</p>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>泛型 + trait bound</th>
      <th>trait 对象 <code class="language-plaintext highlighter-rouge">Box&lt;dyn Draw&gt;</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>元素类型</td>
      <td>必须同质</td>
      <td>可异质混装</td>
    </tr>
    <tr>
      <td>类型集合</td>
      <td>编译期已知</td>
      <td>库使用者可自行扩展</td>
    </tr>
    <tr>
      <td>分发方式</td>
      <td>静态分发（单态化，编译期确定调用哪个方法）</td>
      <td>动态分发（运行时通过指针查表）</td>
    </tr>
    <tr>
      <td>性能</td>
      <td>可内联，有优化空间</td>
      <td>无法内联，丧失部分优化</td>
    </tr>
    <tr>
      <td>适用场景</td>
      <td>同质集合</td>
      <td>需要运行时灵活扩展</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">run</code> 不需要知道组件到底是 <code class="language-plaintext highlighter-rouge">Button</code> 还是 <code class="language-plaintext highlighter-rouge">SelectBox</code>，只关心「能调用 <code class="language-plaintext highlighter-rouge">draw</code>」——这很像动态类型语言里的<strong>鸭子类型</strong>（走起来像鸭子、叫起来像鸭子，那它就是鸭子）。区别在于：动态语言要到运行时才发现「这个对象没有 draw 方法」，而 Rust 在编译期就拦下，比如往 <code class="language-plaintext highlighter-rouge">Screen</code> 里塞一个 <code class="language-plaintext highlighter-rouge">String</code>，会直接报 <code class="language-plaintext highlighter-rouge">error[E0277]: the trait bound 'String: Draw' is not satisfied</code>。</p>

<p>代价是<strong>动态分发</strong>：编译器不知道 trait 对象的具体类型，只能在运行时查方法表，因此无法内联优化。</p>

<p>还有一条硬规则：只有<strong>对象安全</strong>的 trait 才能做成 trait 对象。实践中最常遇到的是——方法不能返回 <code class="language-plaintext highlighter-rouge">Self</code>、不能带泛型参数。因为 trait 对象已经「忘记」了具体类型。典型反例是 <code class="language-plaintext highlighter-rouge">Clone</code>（<code class="language-plaintext highlighter-rouge">fn clone(&amp;self) -&gt; Self</code>），写 <code class="language-plaintext highlighter-rouge">Vec&lt;Box&lt;dyn Clone&gt;&gt;</code> 会报 <code class="language-plaintext highlighter-rouge">error[E0038]: the trait 'Clone' cannot be made into an object</code>。</p>

<h2 id="五状态模式实战一篇博客的发布工作流">五、状态模式实战：一篇博客的发布工作流</h2>

<p><strong>状态模式</strong>的要点是：一个值有内部状态，行为随状态而变；每个状态各自负责自己的行为以及何时转移到下一个状态；持有状态的值对这些细节毫不知情。</p>

<p>需求是这样的：新建的博文是草案 → 请求审核 → 审核通过 → 发布。其他一切非法操作（比如在审核前直接发布）都<strong>不产生效果</strong>。用状态模式实现时，<code class="language-plaintext highlighter-rouge">Post</code> 内部持有一个 trait 对象：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">struct</span> <span class="n">Post</span> <span class="p">{</span>
    <span class="n">state</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">State</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="n">content</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Post</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="n">Post</span> <span class="p">{</span>
        <span class="n">Post</span> <span class="p">{</span>
            <span class="n">state</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">Draft</span> <span class="p">{})),</span>
            <span class="n">content</span><span class="p">:</span> <span class="nn">String</span><span class="p">::</span><span class="nf">new</span><span class="p">(),</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">request_review</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">=</span> <span class="k">self</span><span class="py">.state</span><span class="nf">.take</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">self</span><span class="py">.state</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="n">s</span><span class="nf">.request_review</span><span class="p">())</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">approve</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">=</span> <span class="k">self</span><span class="py">.state</span><span class="nf">.take</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">self</span><span class="py">.state</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="n">s</span><span class="nf">.approve</span><span class="p">())</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">content</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.state</span><span class="nf">.as_ref</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.content</span><span class="p">(</span><span class="k">self</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">trait</span> <span class="n">State</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">request_review</span><span class="p">(</span><span class="k">self</span><span class="p">:</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">Self</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">State</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="k">fn</span> <span class="nf">approve</span><span class="p">(</span><span class="k">self</span><span class="p">:</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">Self</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">State</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="k">fn</span> <span class="n">content</span><span class="o">&lt;</span><span class="nv">'a</span><span class="o">&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">post</span><span class="p">:</span> <span class="o">&amp;</span><span class="nv">'a</span> <span class="n">Post</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="nv">'a</span> <span class="nb">str</span> <span class="p">{</span>
        <span class="s">""</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>几个设计细节值得单独拎出来：</p>

<table>
  <thead>
    <tr>
      <th>手法</th>
      <th>为什么这样写</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">state</code> 用 <code class="language-plaintext highlighter-rouge">Option&lt;Box&lt;dyn State&gt;&gt;</code></td>
      <td>要取走旧状态的所有权，而 Rust 不允许结构体里有「空字段」，于是用 <code class="language-plaintext highlighter-rouge">take()</code> 取出 <code class="language-plaintext highlighter-rouge">Some</code> 留下 <code class="language-plaintext highlighter-rouge">None</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">self: Box&lt;Self&gt;</code></td>
      <td>只能在这个类型的 <code class="language-plaintext highlighter-rouge">Box</code> 上调用，消费旧状态使其失效，返回新状态</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">as_ref().unwrap()</code></td>
      <td>需要的是 <code class="language-plaintext highlighter-rouge">Option</code> 里值的<strong>引用</strong>而非所有权（不能把 <code class="language-plaintext highlighter-rouge">state</code> 移出 <code class="language-plaintext highlighter-rouge">&amp;self</code>）；所有方法都保证返回时是 <code class="language-plaintext highlighter-rouge">Some</code>，所以 <code class="language-plaintext highlighter-rouge">unwrap</code> 不会 panic</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">content</code> 给默认实现 <code class="language-plaintext highlighter-rouge">""</code></td>
      <td><code class="language-plaintext highlighter-rouge">Draft</code>、<code class="language-plaintext highlighter-rouge">PendingReview</code> 直接继承即可，只有 <code class="language-plaintext highlighter-rouge">Published</code> 覆盖成 <code class="language-plaintext highlighter-rouge">&amp;post.content</code>；需要生命周期标注，因为返回值借自参数 <code class="language-plaintext highlighter-rouge">post</code></td>
    </tr>
  </tbody>
</table>

<p>三个状态各自实现转移规则：<code class="language-plaintext highlighter-rouge">Draft::request_review</code> 返回 <code class="language-plaintext highlighter-rouge">PendingReview</code>，<code class="language-plaintext highlighter-rouge">PendingReview::approve</code> 返回 <code class="language-plaintext highlighter-rouge">Published</code>，而 <code class="language-plaintext highlighter-rouge">Draft::approve</code> 和 <code class="language-plaintext highlighter-rouge">Published::*</code> 都返回 <code class="language-plaintext highlighter-rouge">self</code>（非法操作静默无效）。于是「未审核的博文不能显示内容」这条规则，全部集中在状态对象里，<code class="language-plaintext highlighter-rouge">Post</code> 自身一无所知。</p>

<p><strong>权衡取舍</strong>：优点显而易见——要看「已发布博文有哪些行为」，只需看 <code class="language-plaintext highlighter-rouge">Published</code> 一处；新增状态只需加一个 struct 并为其实现 trait，不用到处改 <code class="language-plaintext highlighter-rouge">match</code>。缺点也有三个：状态之间互相耦合（想在 <code class="language-plaintext highlighter-rouge">PendingReview</code> 和 <code class="language-plaintext highlighter-rouge">Published</code> 之间插入 <code class="language-plaintext highlighter-rouge">Scheduled</code>，就得改 <code class="language-plaintext highlighter-rouge">PendingReview</code> 的代码）；存在重复逻辑（想给返回 <code class="language-plaintext highlighter-rouge">self</code> 的方法加默认实现会破坏对象安全）；<code class="language-plaintext highlighter-rouge">Post</code> 里 <code class="language-plaintext highlighter-rouge">request_review</code>/<code class="language-plaintext highlighter-rouge">approve</code> 的模板化代码，方法多了可以考虑用宏消除。</p>

<h2 id="六更好的一种可能把状态编码进类型">六、更好的一种可能：把状态编码进类型</h2>

<p>既然目标是「非法状态不可达」，那不如让类型系统直接拦住它。换个思路：<code class="language-plaintext highlighter-rouge">Post::new()</code> 返回的不是 <code class="language-plaintext highlighter-rouge">Post</code> 而是 <code class="language-plaintext highlighter-rouge">DraftPost</code>，而 <strong><code class="language-plaintext highlighter-rouge">DraftPost</code> 根本没有 <code class="language-plaintext highlighter-rouge">content</code> 方法</strong>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">struct</span> <span class="n">Post</span> <span class="p">{</span>
    <span class="n">content</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">struct</span> <span class="n">DraftPost</span> <span class="p">{</span>
    <span class="n">content</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Post</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="n">DraftPost</span> <span class="p">{</span>
        <span class="n">DraftPost</span> <span class="p">{</span> <span class="n">content</span><span class="p">:</span> <span class="nn">String</span><span class="p">::</span><span class="nf">new</span><span class="p">()</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">content</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="p">{</span>
        <span class="o">&amp;</span><span class="k">self</span><span class="py">.content</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">DraftPost</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">add_text</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.content</span><span class="nf">.push_str</span><span class="p">(</span><span class="n">text</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">request_review</span><span class="p">(</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">PendingReviewPost</span> <span class="p">{</span>
        <span class="n">PendingReviewPost</span> <span class="p">{</span> <span class="n">content</span><span class="p">:</span> <span class="k">self</span><span class="py">.content</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">struct</span> <span class="n">PendingReviewPost</span> <span class="p">{</span>
    <span class="n">content</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">PendingReviewPost</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">approve</span><span class="p">(</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Post</span> <span class="p">{</span>
        <span class="n">Post</span> <span class="p">{</span> <span class="n">content</span><span class="p">:</span> <span class="k">self</span><span class="py">.content</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">request_review</code> 和 <code class="language-plaintext highlighter-rouge">approve</code> 都获取 <code class="language-plaintext highlighter-rouge">self</code> 的所有权，消费掉旧实例、返回新类型。于是「拿到能读 <code class="language-plaintext highlighter-rouge">content</code> 的 <code class="language-plaintext highlighter-rouge">Post</code>」的唯一路径，就是 <code class="language-plaintext highlighter-rouge">DraftPost → request_review → PendingReviewPost → approve → Post</code>。想显示草案内容？那行代码压根编译不过。调用方相应要重新绑定变量：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="k">mut</span> <span class="n">post</span> <span class="o">=</span> <span class="nn">Post</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="n">post</span><span class="nf">.add_text</span><span class="p">(</span><span class="s">"I ate a salad for lunch today"</span><span class="p">);</span>
<span class="k">let</span> <span class="n">post</span> <span class="o">=</span> <span class="n">post</span><span class="nf">.request_review</span><span class="p">();</span>
<span class="k">let</span> <span class="n">post</span> <span class="o">=</span> <span class="n">post</span><span class="nf">.approve</span><span class="p">();</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="s">"I ate a salad for lunch today"</span><span class="p">,</span> <span class="n">post</span><span class="nf">.content</span><span class="p">());</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>状态模式（trait 对象）</th>
      <th>状态编码为类型</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>非法状态</td>
      <td>运行时静默无效</td>
      <td>编译期错误，根本无法表示</td>
    </tr>
    <tr>
      <td>转移封装</td>
      <td>完全封装在 <code class="language-plaintext highlighter-rouge">Post</code> 内部</td>
      <td>暴露给用户，需 <code class="language-plaintext highlighter-rouge">let post = ...</code> 重新绑定</td>
    </tr>
    <tr>
      <td>扩展方式</td>
      <td>新增一个状态 struct</td>
      <td>新增类型并调整转换链</td>
    </tr>
    <tr>
      <td>代价</td>
      <td>状态间耦合、部分重复代码</td>
      <td>严格来说不再是「状态模式」</td>
    </tr>
  </tbody>
</table>

<h2 id="实践建议">实践建议</h2>

<ol>
  <li><strong>同质用泛型，异质用 trait 对象</strong>。集合元素类型一致且编译期已知，就用泛型 + trait bound 享受静态分发；需要让使用者自行扩展类型集合，再用 <code class="language-plaintext highlighter-rouge">Box&lt;dyn Trait&gt;</code>，并坦然接受动态分发的那点开销。</li>
  <li><strong>让非法状态无法被表示</strong>。能用类型系统在编译期拦住的 bug，就不要留到运行时去「返回空字符串」。这是 Rust 相比传统 OOP 的额外红利。</li>
  <li><strong>别生搬设计模式</strong>。Rust 拥有所有权、trait、泛型这些面向对象语言没有的武器，照搬 OOP 模式未必是最优解——先问一句「这个约束能不能交给编译器」。</li>
</ol>

<h2 id="小结">小结</h2>

<p>trait 对象是 Rust 获取部分面向对象能力的手段：用少量运行时性能换取灵活性，进而支撑状态模式这类可维护性模式。但同时，Rust 也提供了「把状态编码进类型」这种更彻底的方案——它不是面向对象模式，却能把 bug 挡在部署之前。这正是本章的落点：<strong>面向对象模式在 Rust 中始终可用，但并不总是最佳选择</strong>。</p>

<p>下一篇进入第 18 章「模式和匹配」，那些散落全书的 <code class="language-plaintext highlighter-rouge">match</code>、<code class="language-plaintext highlighter-rouge">if let</code>，终于要被系统地讲一次了。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="rust" /><category term="oop" /><category term="trait-object" /><category term="dyn" /><category term="state-pattern" /><category term="design-patterns" /><summary type="html"><![CDATA[对应书源：《Rust 程序设计语言》第 17 章「Rust 的面向对象特性」]]></summary></entry><entry><title type="html">Rust 学习笔记（17/21）：无畏并发——编译器替你排查数据竞争</title><link href="https://0end1.github.io/2026/09/17/rust-fearless-concurrency/" rel="alternate" type="text/html" title="Rust 学习笔记（17/21）：无畏并发——编译器替你排查数据竞争" /><published>2026-09-17T09:00:00+08:00</published><updated>2026-09-17T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/17/rust-fearless-concurrency</id><content type="html" xml:base="https://0end1.github.io/2026/09/17/rust-fearless-concurrency/"><![CDATA[<blockquote>
  <p>对应书源：《Rust 程序设计语言》第 16 章「无畏并发」</p>
</blockquote>

<h2 id="开篇并发编程的噩梦与-rust-的解法">开篇：并发编程的「噩梦」与 Rust 的解法</h2>

<p>多线程编程是许多开发者的噩梦。在 C/C++ 中，一个数据竞争（data race）可能潜伏数月，直到某个特定时机才 crash；在 Java 里，虽然内存模型定义了 happen-before 规则，但死锁和活锁依然防不胜防。调试这些 bug 就像在黑暗中捉迷藏——你知道它在那里，但永远不确定何时会撞上。</p>

<p>Rust 对这个问题给出了一个激进的答案：<strong>把并发错误变成编译错误</strong>。通过所有权和类型系统，Rust 在编译期就拒绝那些有数据竞争风险的代码。这不是魔法，而是把第 4 章的所有权规则、第 15 章的智能指针，一并延伸到了多线程场景。官方给这个特性起了个响亮的名字——<strong>无畏并发</strong>（fearless concurrency）。</p>

<h2 id="一线程基础spawnjoin-与-move-闭包">一、线程基础：spawn、join 与 move 闭包</h2>

<h3 id="11-创建线程与等待结束">1.1 创建线程与等待结束</h3>

<p>Rust 标准库提供 1:1 线程模型（一个语言线程对应一个 OS 线程），通过 <code class="language-plaintext highlighter-rouge">thread::spawn</code> 创建：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="n">thread</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">time</span><span class="p">::</span><span class="n">Duration</span><span class="p">;</span>

<span class="k">let</span> <span class="n">handle</span> <span class="o">=</span> <span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(||</span> <span class="p">{</span>
    <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">10</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"hi number {} from the spawned thread!"</span><span class="p">,</span> <span class="n">i</span><span class="p">);</span>
        <span class="nn">thread</span><span class="p">::</span><span class="nf">sleep</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_millis</span><span class="p">(</span><span class="mi">1</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="c1">// 主线程继续执行自己的代码</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">5</span> <span class="p">{</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"hi number {} from the main thread!"</span><span class="p">,</span> <span class="n">i</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// 阻塞等待子线程结束</span>
<span class="n">handle</span><span class="nf">.join</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">spawn</code> 返回一个 <code class="language-plaintext highlighter-rouge">JoinHandle</code>，调用其 <code class="language-plaintext highlighter-rouge">join</code> 方法会阻塞当前线程直到子线程结束。<strong><code class="language-plaintext highlighter-rouge">join</code> 放的位置很关键</strong>：放在主线程的 for 循环之前，子线程会先跑完；放在之后，两者交替执行。</p>

<h3 id="12-move-闭包把所有权搬进线程">1.2 move 闭包：把所有权「搬」进线程</h3>

<p>如果闭包要捕获主线程的数据，默认借用可能不安全——Rust 不知道子线程会运行多久。此时需要 <code class="language-plaintext highlighter-rouge">move</code> 关键字强制转移所有权：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">v</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">];</span>

<span class="k">let</span> <span class="n">handle</span> <span class="o">=</span> <span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">move</span> <span class="p">||</span> <span class="p">{</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"Here's a vector: {:?}"</span><span class="p">,</span> <span class="n">v</span><span class="p">);</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>不加 <code class="language-plaintext highlighter-rouge">move</code> 时编译器报错 <code class="language-plaintext highlighter-rouge">E0373</code>：closure may outlive borrowed value。加了 <code class="language-plaintext highlighter-rouge">move</code> 后，<code class="language-plaintext highlighter-rouge">v</code> 的所有权归子线程所有，主线程不能再使用它——<strong>所有权规则在线程间依然生效</strong>。</p>

<h2 id="二消息传递不要共享内存要通过通讯共享内存">二、消息传递：不要共享内存，要通过通讯共享内存</h2>

<p>Go 语言的口号「不要通过共享内存来通讯；而是通过通讯来共享内存」在 Rust 中也有完美实现——<strong>通道</strong>（channel）。</p>

<h3 id="21-mpsc-通道的基本用法">2.1 mpsc 通道的基本用法</h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="n">mpsc</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="n">thread</span><span class="p">;</span>

<span class="k">let</span> <span class="p">(</span><span class="n">tx</span><span class="p">,</span> <span class="n">rx</span><span class="p">)</span> <span class="o">=</span> <span class="nn">mpsc</span><span class="p">::</span><span class="nf">channel</span><span class="p">();</span> <span class="c1">// multiple producer, single consumer</span>

<span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">move</span> <span class="p">||</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">val</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"hi"</span><span class="p">);</span>
    <span class="n">tx</span><span class="nf">.send</span><span class="p">(</span><span class="n">val</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span> <span class="c1">// send 获取所有权并转移给接收端</span>
<span class="p">});</span>

<span class="k">let</span> <span class="n">received</span> <span class="o">=</span> <span class="n">rx</span><span class="nf">.recv</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span> <span class="c1">// 阻塞直到收到值</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"Got: {}"</span><span class="p">,</span> <span class="n">received</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">mpsc::channel()</code> 返回发送端 <code class="language-plaintext highlighter-rouge">tx</code> 和接收端 <code class="language-plaintext highlighter-rouge">rx</code>。关键点：<strong><code class="language-plaintext highlighter-rouge">send</code> 会拿走值的所有权</strong>，发送后发送方不能再使用该值，否则报 <code class="language-plaintext highlighter-rouge">E0382</code> use of moved value。</p>

<h3 id="22-接收端的两种方式">2.2 接收端的两种方式</h3>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>行为</th>
      <th>适用场景</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">recv()</code></td>
      <td>阻塞直到收到值，通道关闭时返回 <code class="language-plaintext highlighter-rouge">Err</code></td>
      <td>主线程无事可做，专心等待</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">try_recv()</code></td>
      <td>立即返回，有值则 <code class="language-plaintext highlighter-rouge">Ok</code>，无值则 <code class="language-plaintext highlighter-rouge">Err</code></td>
      <td>接收线程还要做其他工作，轮询检查</td>
    </tr>
  </tbody>
</table>

<h3 id="23-多生产者克隆发送端">2.3 多生产者：克隆发送端</h3>

<p><code class="language-plaintext highlighter-rouge">mpsc</code> 允许多个发送端，通过 <code class="language-plaintext highlighter-rouge">clone</code> 实现：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">tx1</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.clone</span><span class="p">();</span>
<span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">move</span> <span class="p">||</span> <span class="p">{</span> <span class="n">tx1</span><span class="nf">.send</span><span class="p">(</span><span class="o">...</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span> <span class="p">});</span>
<span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">move</span> <span class="p">||</span> <span class="p">{</span> <span class="n">tx</span><span class="nf">.send</span><span class="p">(</span><span class="o">...</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span> <span class="p">});</span>

<span class="k">for</span> <span class="n">received</span> <span class="k">in</span> <span class="n">rx</span> <span class="p">{</span> <span class="c1">// rx 可作为迭代器，通道关闭时结束</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"Got: {}"</span><span class="p">,</span> <span class="n">received</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>多个线程并发发送，接收端按到达顺序处理。输出顺序每次可能不同，这是并发的本质特性。</p>

<h2 id="三共享状态mutex--arc">三、共享状态：Mutex<T> + Arc<T></T></T></h2>

<p>通道类似单所有权，而共享内存需要多所有权。Rust 的方案是 <strong>互斥器</strong>（mutex）。</p>

<h3 id="31-mutex获取锁才能访问">3.1 Mutex<T>：获取锁才能访问</T></h3>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="n">Mutex</span><span class="p">;</span>

<span class="k">let</span> <span class="n">m</span> <span class="o">=</span> <span class="nn">Mutex</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>
<span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">num</span> <span class="o">=</span> <span class="n">m</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span> <span class="c1">// 获取锁，阻塞直到成功</span>
    <span class="o">*</span><span class="n">num</span> <span class="o">=</span> <span class="mi">6</span><span class="p">;</span> <span class="c1">// num 是 MutexGuard，解引用后可变访问内部值</span>
<span class="p">}</span> <span class="c1">// 离开作用域自动释放锁（Drop 实现）</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Mutex&lt;T&gt;</code> 是一个智能指针：<code class="language-plaintext highlighter-rouge">lock()</code> 返回 <code class="language-plaintext highlighter-rouge">MutexGuard</code>，它实现了 <code class="language-plaintext highlighter-rouge">Deref</code> 指向内部数据，并在离开作用域时自动释放锁。类型系统保证<strong>不获取锁就无法访问内部数据</strong>。</p>

<h3 id="32-多线程共享rc-不行arc-才行">3.2 多线程共享：Rc<T> 不行，Arc<T> 才行</T></T></h3>

<p>直觉上，用 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 包一层 <code class="language-plaintext highlighter-rouge">Mutex&lt;T&gt;</code> 就可以多线程共享了：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">counter</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">Mutex</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
<span class="c1">// ...</span>
<span class="k">let</span> <span class="n">counter</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">counter</span><span class="p">);</span>
<span class="k">let</span> <span class="n">handle</span> <span class="o">=</span> <span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">move</span> <span class="p">||</span> <span class="p">{</span> <span class="o">...</span> <span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>但编译报错：<code class="language-plaintext highlighter-rouge">Rc&lt;Mutex&lt;i32&gt;&gt; cannot be sent between threads safely</code>。原因是 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 的引用计数不是线程安全的，多个线程同时增减计数会数据竞争。</p>

<p>解决方案是 <strong><code class="language-plaintext highlighter-rouge">Arc&lt;T&gt;</code></strong>（atomically reference counted），API 与 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 相同，但计数操作是原子的：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::{</span><span class="n">Mutex</span><span class="p">,</span> <span class="nb">Arc</span><span class="p">};</span>

<span class="k">let</span> <span class="n">counter</span> <span class="o">=</span> <span class="nn">Arc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">Mutex</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">handles</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>

<span class="k">for</span> <span class="n">_</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="mi">10</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">counter</span> <span class="o">=</span> <span class="nn">Arc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">counter</span><span class="p">);</span>
    <span class="k">let</span> <span class="n">handle</span> <span class="o">=</span> <span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">move</span> <span class="p">||</span> <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">num</span> <span class="o">=</span> <span class="n">counter</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
        <span class="o">*</span><span class="n">num</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="n">handles</span><span class="nf">.push</span><span class="p">(</span><span class="n">handle</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">for</span> <span class="n">h</span> <span class="k">in</span> <span class="n">handles</span> <span class="p">{</span> <span class="n">h</span><span class="nf">.join</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span> <span class="p">}</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"Result: {}"</span><span class="p">,</span> <span class="o">*</span><span class="n">counter</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">());</span> <span class="c1">// 10</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>组合</th>
      <th>用途</th>
      <th>线程安全</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code></td>
      <td>单线程多所有权</td>
      <td>❌ 非 Send</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Arc&lt;T&gt;</code></td>
      <td>多线程多所有权</td>
      <td>✅ Send + Sync</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code></td>
      <td>单线程内部可变性</td>
      <td>❌ 非 Sync</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Mutex&lt;T&gt;</code></td>
      <td>多线程内部可变性</td>
      <td>✅ Sync</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Rc&lt;RefCell&lt;T&gt;&gt;</code></td>
      <td>单线程可变共享</td>
      <td>❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Arc&lt;Mutex&lt;T&gt;&gt;</code></td>
      <td>多线程可变共享</td>
      <td>✅</td>
    </tr>
  </tbody>
</table>

<h3 id="33-死锁的提醒">3.3 死锁的提醒</h3>

<p>Rust 的类型系统能防止数据竞争，但<strong>无法阻止逻辑层面的死锁</strong>。如果线程 A 持有锁 X 等待锁 Y，同时线程 B 持有锁 Y 等待锁 X，就会永远阻塞。规避策略与其他语言相同：统一加锁顺序、使用超时等。</p>

<h2 id="四send-与-sync并发的类型护照">四、Send 与 Sync：并发的「类型护照」</h2>

<p>Rust 并发模型的精髓在于两个<strong>标记 trait</strong>：</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Send</code></strong>：类型可以安全地<strong>转移所有权</strong>到另一个线程。几乎所有类型都是 <code class="language-plaintext highlighter-rouge">Send</code>，除了 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code>（引用计数非原子）和裸指针。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Sync</code></strong>：类型可以安全地<strong>被多个线程同时引用</strong>（即 <code class="language-plaintext highlighter-rouge">&amp;T</code> 是 <code class="language-plaintext highlighter-rouge">Send</code>）。<code class="language-plaintext highlighter-rouge">Mutex&lt;T&gt;</code> 是 <code class="language-plaintext highlighter-rouge">Sync</code> 的，<code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code> 和 <code class="language-plaintext highlighter-rouge">Cell&lt;T&gt;</code> 不是（运行时借用检查非线程安全）。</li>
</ul>

<p><strong>自动推导规则</strong>：完全由 <code class="language-plaintext highlighter-rouge">Send</code>/<code class="language-plaintext highlighter-rouge">Sync</code> 类型组成的类型，自动也是 <code class="language-plaintext highlighter-rouge">Send</code>/<code class="language-plaintext highlighter-rouge">Sync</code>。几乎不需要手动实现——手动实现需要 <code class="language-plaintext highlighter-rouge">unsafe</code> 代码。</p>

<table>
  <thead>
    <tr>
      <th>trait</th>
      <th>含义</th>
      <th>反例</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Send</code></td>
      <td>所有权可跨线程转移</td>
      <td><code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code>、裸指针</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Sync</code></td>
      <td>可被多线程共享引用</td>
      <td><code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code>、<code class="language-plaintext highlighter-rouge">Cell&lt;T&gt;</code>、<code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code></td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">thread::spawn</code> 的签名中要求闭包实现 <code class="language-plaintext highlighter-rouge">Send</code>，这就是为什么 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 无法直接传给 <code class="language-plaintext highlighter-rouge">spawn</code>。编译器在类型层面「发护照」：没 <code class="language-plaintext highlighter-rouge">Send</code>「签证」的就不能过境到别的线程。</p>

<h2 id="五实践建议">五、实践建议</h2>

<ol>
  <li><strong>优先用消息传递，次选共享状态</strong>：通道让数据所有权流向清晰；共享状态只在性能必要或已有架构需要时才考虑。</li>
  <li><strong>共享状态就用 <code class="language-plaintext highlighter-rouge">Arc&lt;Mutex&lt;T&gt;&gt;</code></strong>：这是 Rust 多线程可变共享的标准配方，别试图用 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 绕过——编译器不答应。</li>
  <li><strong>锁的粒度尽量小</strong>：把 <code class="language-plaintext highlighter-rouge">lock()</code> 调用和后续操作限制在最小的作用域内，减少竞争窗口。别让锁跨过 <code class="language-plaintext highlighter-rouge">await</code> 或阻塞调用。</li>
</ol>

<h2 id="六总结">六、总结</h2>

<table>
  <thead>
    <tr>
      <th>并发模型</th>
      <th>Rust 工具</th>
      <th>核心保障</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>创建线程</td>
      <td><code class="language-plaintext highlighter-rouge">thread::spawn</code> + <code class="language-plaintext highlighter-rouge">JoinHandle</code></td>
      <td>所有权规则在线程间生效</td>
    </tr>
    <tr>
      <td>跨线程传数据</td>
      <td><code class="language-plaintext highlighter-rouge">move</code> 闭包</td>
      <td>编译期确保无悬垂引用</td>
    </tr>
    <tr>
      <td>消息传递</td>
      <td><code class="language-plaintext highlighter-rouge">mpsc::channel</code></td>
      <td><code class="language-plaintext highlighter-rouge">send</code> 转移所有权，防止用后用</td>
    </tr>
    <tr>
      <td>共享可变状态</td>
      <td><code class="language-plaintext highlighter-rouge">Arc&lt;Mutex&lt;T&gt;&gt;</code></td>
      <td>类型系统确保获取锁才能访问</td>
    </tr>
    <tr>
      <td>线程安全标记</td>
      <td><code class="language-plaintext highlighter-rouge">Send</code> / <code class="language-plaintext highlighter-rouge">Sync</code></td>
      <td>自动推导，禁止不安全的跨线程传递</td>
    </tr>
  </tbody>
</table>

<p>Rust 的并发不是通过增加运行时复杂度来换取安全，而是<strong>把第 4 章的所有权、第 15 章的智能指针，自然延伸到了多线程</strong>。你写的并发代码一旦通过编译，就从根本上排除了数据竞争和悬垂引用——这正是「无畏」二字的含义。</p>

<p>下一篇将进入第 17 章，聊聊 Rust 的面向对象特性——trait 与面向对象设计模式的碰撞。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="rust" /><category term="concurrency" /><category term="threading" /><category term="mutex" /><category term="arc" /><category term="channel" /><summary type="html"><![CDATA[对应书源：《Rust 程序设计语言》第 16 章「无畏并发」]]></summary></entry><entry><title type="html">Rust 学习笔记（16/21）：智能指针——当编译期规则需要一点「弹性」</title><link href="https://0end1.github.io/2026/09/15/rust-smart-pointers/" rel="alternate" type="text/html" title="Rust 学习笔记（16/21）：智能指针——当编译期规则需要一点「弹性」" /><published>2026-09-15T09:00:00+08:00</published><updated>2026-09-15T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/15/rust-smart-pointers</id><content type="html" xml:base="https://0end1.github.io/2026/09/15/rust-smart-pointers/"><![CDATA[<h1 id="rust-学习笔记1621智能指针当编译期规则需要一点弹性">Rust 学习笔记（16/21）：智能指针——当编译期规则需要一点「弹性」</h1>

<blockquote>
  <p>前几章我们反复看到 Rust 用所有权和借用规则在编译期杜绝内存问题：一个值只有一个所有者，引用要么多个不可变、要么一个可变。这些规则在绝大多数情况下非常清晰。但当数据结构本身需要”多个入口共享同一块内存”，或者”不可变外壳里藏着可变内核”时，编译器的严格有时会成为表达能力的障碍。智能指针就是 Rust 提供的标准答案：它们仍然遵守安全规则，只是把检查的场合从”编译时”挪到了”运行时”，或者通过额外的元数据（引用计数）扩展了所有权的语义。</p>
</blockquote>

<h2 id="开篇引用只是借智能指针是拥有">开篇：引用只是「借」，智能指针是「拥有」</h2>

<p>第4章的引用 <code class="language-plaintext highlighter-rouge">&amp;T</code> 只借用数据，没有任何额外开销。智能指针则是一类<strong>拥有</strong>它们指向的数据，并附带额外元数据或功能的数据结构。<code class="language-plaintext highlighter-rouge">String</code> 和 <code class="language-plaintext highlighter-rouge">Vec&lt;T&gt;</code> 其实也算智能指针——它们拥有数据、管理容量、保证 UTF-8 或内存连续性。</p>

<p>标准库中最常用的三个是 <code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code>、<code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 和 <code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code>，而理解它们的关键在于两个 trait：</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Deref</code></strong>：让智能指针可以像引用一样用 <code class="language-plaintext highlighter-rouge">*</code> 解引用</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Drop</code></strong>：让智能指针在离开作用域时自动执行清理代码</li>
</ul>

<table>
  <thead>
    <tr>
      <th>类型</th>
      <th>核心能力</th>
      <th>所有权模式</th>
      <th>适用场景</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code></td>
      <td>堆分配</td>
      <td>单一所有者</td>
      <td>递归类型、大数据转移、trait 对象</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code></td>
      <td>引用计数</td>
      <td>多个所有者（只读）</td>
      <td>图结构多节点共享、树的多父引用</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code></td>
      <td>内部可变性</td>
      <td>单一所有者（运行时借用检查）</td>
      <td>需要可变借用的 mock 对象、与 <code class="language-plaintext highlighter-rouge">Rc</code> 组合</td>
    </tr>
  </tbody>
</table>

<h2 id="一box把数据放到堆上">一、Box<T>：把数据放到堆上</T></h2>

<p><code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code> 是最简单的智能指针，功能只有两个：在堆上分配数据，以及留在栈上的指针。没有额外开销。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">b</span> <span class="o">=</span> <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"b = {}"</span><span class="p">,</span> <span class="n">b</span><span class="p">);</span>  <span class="c1">// 像访问栈数据一样自然</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code> 真正的价值在于<strong>递归类型</strong>。Rust 需要在编译时知道类型大小，而递归类型理论上可以无限嵌套，大小未知。cons list（Lisp 风格链表）是典型的例子：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">// 编译错误：recursive type `List` has infinite size</span>
<span class="k">enum</span> <span class="n">List</span> <span class="p">{</span>
    <span class="nf">Cons</span><span class="p">(</span><span class="nb">i32</span><span class="p">,</span> <span class="n">List</span><span class="p">),</span>
    <span class="nb">Nil</span><span class="p">,</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>把 <code class="language-plaintext highlighter-rouge">List</code> 换成 <code class="language-plaintext highlighter-rouge">Box&lt;List&gt;</code>，问题就解决了——<code class="language-plaintext highlighter-rouge">Box</code> 是指针，大小固定（平台指针宽度），编译器能算出 <code class="language-plaintext highlighter-rouge">Cons</code> 需要 <code class="language-plaintext highlighter-rouge">i32 + box 指针</code> 的空间，递归链被打破：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">enum</span> <span class="n">List</span> <span class="p">{</span>
    <span class="nf">Cons</span><span class="p">(</span><span class="nb">i32</span><span class="p">,</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="n">List</span><span class="o">&gt;</span><span class="p">),</span>
    <span class="nb">Nil</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">let</span> <span class="n">list</span> <span class="o">=</span> <span class="nf">Cons</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span>
    <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nf">Cons</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span>
        <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nf">Cons</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span>
            <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nb">Nil</span><span class="p">))))));</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p>虽然函数式语言常用 cons list，Rust 里 <code class="language-plaintext highlighter-rouge">Vec&lt;T&gt;</code> 是更好的选择。这里用它只是因为概念简单，能清晰展示递归类型与 <code class="language-plaintext highlighter-rouge">Box</code> 的关系。</p>
</blockquote>

<h2 id="二deref-trait让自定义类型也能用-">二、Deref trait：让自定义类型也能用 <code class="language-plaintext highlighter-rouge">*</code></h2>

<p><code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code> 能像引用一样用 <code class="language-plaintext highlighter-rouge">*y</code> 解引用，是因为它实现了 <code class="language-plaintext highlighter-rouge">Deref</code> trait。我们自己也可以做到。</p>

<p>实现 <code class="language-plaintext highlighter-rouge">Deref</code> 只需要提供一个 <code class="language-plaintext highlighter-rouge">deref</code> 方法，返回内部数据的引用：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">ops</span><span class="p">::</span><span class="n">Deref</span><span class="p">;</span>

<span class="k">struct</span> <span class="n">MyBox</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">T</span><span class="p">);</span>

<span class="k">impl</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">Deref</span> <span class="k">for</span> <span class="n">MyBox</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">type</span> <span class="n">Target</span> <span class="o">=</span> <span class="n">T</span><span class="p">;</span>
    <span class="k">fn</span> <span class="nf">deref</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="n">T</span> <span class="p">{</span>
        <span class="o">&amp;</span><span class="k">self</span><span class="na">.0</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>当写下 <code class="language-plaintext highlighter-rouge">*y</code> 时，Rust 实际上执行的是 <code class="language-plaintext highlighter-rouge">*(y.deref())</code>。<code class="language-plaintext highlighter-rouge">deref</code> 返回引用而非值，是为了不转移所有权。</p>

<h3 id="解引用强制转换deref-coercions">解引用强制转换（Deref Coercions）</h3>

<p>这是 <code class="language-plaintext highlighter-rouge">Deref</code> 带来的更实用的功能。当函数参数类型是 <code class="language-plaintext highlighter-rouge">&amp;str</code> 而传入 <code class="language-plaintext highlighter-rouge">&amp;MyBox&lt;String&gt;</code> 时，Rust 会自动链式调用 <code class="language-plaintext highlighter-rouge">deref</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">hello</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="k">let</span> <span class="n">m</span> <span class="o">=</span> <span class="nn">MyBox</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"Rust"</span><span class="p">));</span>
<span class="nf">hello</span><span class="p">(</span><span class="o">&amp;</span><span class="n">m</span><span class="p">);</span>  <span class="c1">// 自动：&amp;MyBox&lt;String&gt; → &amp;String → &amp;str</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>没有强制转换，你得写 <code class="language-plaintext highlighter-rouge">hello(&amp;(*m)[..])</code>。强制转换发生在编译期，零运行时开销。</p>

<table>
  <thead>
    <tr>
      <th>转换方向</th>
      <th>条件</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&amp;T</code> → <code class="language-plaintext highlighter-rouge">&amp;U</code></td>
      <td><code class="language-plaintext highlighter-rouge">T: Deref&lt;Target=U&gt;</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&amp;mut T</code> → <code class="language-plaintext highlighter-rouge">&amp;mut U</code></td>
      <td><code class="language-plaintext highlighter-rouge">T: DerefMut&lt;Target=U&gt;</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&amp;mut T</code> → <code class="language-plaintext highlighter-rouge">&amp;U</code></td>
      <td><code class="language-plaintext highlighter-rouge">T: Deref&lt;Target=U&gt;</code>（可变→不可变永远安全）</td>
    </tr>
  </tbody>
</table>

<p>注意反向不可能：不可变引用不能强转为可变引用，因为借用规则无法保证唯一性。</p>

<h2 id="三drop-trait离开作用域时自动收尾">三、Drop trait：离开作用域时自动「收尾」</h2>

<p><code class="language-plaintext highlighter-rouge">Drop</code> trait 让你定义值离开作用域时要执行的清理逻辑。<code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code> 用它来释放堆内存，你也可以用来关闭文件或网络连接。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">struct</span> <span class="n">CustomSmartPointer</span> <span class="p">{</span>
    <span class="n">data</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="nb">Drop</span> <span class="k">for</span> <span class="n">CustomSmartPointer</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">drop</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Dropping `{}`"</span><span class="p">,</span> <span class="k">self</span><span class="py">.data</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">c</span> <span class="o">=</span> <span class="n">CustomSmartPointer</span> <span class="p">{</span> <span class="n">data</span><span class="p">:</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"my stuff"</span><span class="p">)</span> <span class="p">};</span>
    <span class="k">let</span> <span class="n">d</span> <span class="o">=</span> <span class="n">CustomSmartPointer</span> <span class="p">{</span> <span class="n">data</span><span class="p">:</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"other stuff"</span><span class="p">)</span> <span class="p">};</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"Created."</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// 输出顺序：Created. → Dropping `other stuff` → Dropping `my stuff`</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>变量以<strong>创建顺序的相反顺序</strong>被丢弃，所以 <code class="language-plaintext highlighter-rouge">d</code> 先于 <code class="language-plaintext highlighter-rouge">c</code>。</p>

<h3 id="提前丢弃stdmemdrop">提前丢弃：std::mem::drop</h3>

<p>不能手动调用 <code class="language-plaintext highlighter-rouge">c.drop()</code>——Rust 禁止显式析构，因为结束时还会自动再调用一次，导致 double free。如果确实需要提前清理，用标准库的 <code class="language-plaintext highlighter-rouge">std::mem::drop</code> 函数（注意是小写 <code class="language-plaintext highlighter-rouge">drop</code>，不是 <code class="language-plaintext highlighter-rouge">Drop</code> trait 的方法）：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nf">drop</span><span class="p">(</span><span class="n">c</span><span class="p">);</span>  <span class="c1">// prelude 已导入，提前释放</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"CustomSmartPointer dropped before end of main."</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="四rc一个值多个所有者">四、Rc<T>：一个值，多个所有者</T></h2>

<p>有些场景天然需要多所有权。比如图结构中多个边指向同一个节点，节点应该直到没有边指向它时才被释放。<code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code>（reference counting）就是为此设计：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">rc</span><span class="p">::</span><span class="nb">Rc</span><span class="p">;</span>

<span class="k">enum</span> <span class="n">List</span> <span class="p">{</span>
    <span class="nf">Cons</span><span class="p">(</span><span class="nb">i32</span><span class="p">,</span> <span class="nb">Rc</span><span class="o">&lt;</span><span class="n">List</span><span class="o">&gt;</span><span class="p">),</span>
    <span class="nb">Nil</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">let</span> <span class="n">a</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nf">Cons</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nf">Cons</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nb">Nil</span><span class="p">)))));</span>
<span class="k">let</span> <span class="n">b</span> <span class="o">=</span> <span class="nf">Cons</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="p">));</span>
<span class="k">let</span> <span class="n">c</span> <span class="o">=</span> <span class="nf">Cons</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="p">));</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Rc::clone(&amp;a)</code> 不会深拷贝数据，只是把引用计数加 1。标准库故意让 <code class="language-plaintext highlighter-rouge">Rc::clone</code> 与深拷贝的 <code class="language-plaintext highlighter-rouge">.clone()</code> 同名但语义不同，这是 Rust 社区的习惯：看到 <code class="language-plaintext highlighter-rouge">Rc::clone</code> 就知道”只是增计数，不用担心性能”。</p>

<p>用 <code class="language-plaintext highlighter-rouge">Rc::strong_count(&amp;a)</code> 可以观察计数变化：创建 <code class="language-plaintext highlighter-rouge">a</code> 时为 1，克隆给 <code class="language-plaintext highlighter-rouge">b</code> 后为 2，给 <code class="language-plaintext highlighter-rouge">c</code> 后为 3，<code class="language-plaintext highlighter-rouge">c</code> 离开作用域后自动减回 2。</p>

<blockquote>
  <p>⚠️ <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> <strong>只能用于单线程</strong>。多线程的引用计数在第16章讲。</p>
</blockquote>

<h2 id="五refcell编译器信不过你你自己来保证">五、RefCell<T>：编译器信不过你，你自己来保证</T></h2>

<p><code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 解决了多所有权，但它只提供不可变访问。如果要修改共享数据怎么办？<code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code> 引入<strong>内部可变性</strong>（interior mutability）：外表不可变，内部可变。</p>

<p><code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code> 的借用规则不在编译期检查，而在<strong>运行时</strong>检查。违反规则不会编译错误，而是直接 panic：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">cell</span><span class="p">::</span><span class="n">RefCell</span><span class="p">;</span>

<span class="k">let</span> <span class="n">cell</span> <span class="o">=</span> <span class="nn">RefCell</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nd">vec!</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]);</span>
<span class="n">cell</span><span class="nf">.borrow_mut</span><span class="p">()</span><span class="nf">.push</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>     <span class="c1">// 可变借用，OK</span>
<span class="n">cell</span><span class="nf">.borrow</span><span class="p">()</span><span class="nf">.len</span><span class="p">();</span>           <span class="c1">// 不可变借用，OK</span>
<span class="c1">// 同时两个 borrow_mut() → panic: already borrowed: BorrowMutError</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">borrow()</code> 返回 <code class="language-plaintext highlighter-rouge">Ref&lt;T&gt;</code>，<code class="language-plaintext highlighter-rouge">borrow_mut()</code> 返回 <code class="language-plaintext highlighter-rouge">RefMut&lt;T&gt;</code>，两者都实现了 <code class="language-plaintext highlighter-rouge">Deref</code>，可以像普通引用一样用。<code class="language-plaintext highlighter-rouge">RefCell</code> 内部维护活跃借用的计数，规则与编译期完全一致：多个不可变 ✅，单个可变 ✅，多个可变 ❌，可变+不可变 ❌。</p>

<h3 id="mock-对象的经典用例">Mock 对象的经典用例</h3>

<p>这是书里最有说服力的例子。假设有个 <code class="language-plaintext highlighter-rouge">Messenger</code> trait，方法签名要求 <code class="language-plaintext highlighter-rouge">&amp;self</code>（不可变），但测试时 mock 对象需要记录调用过的消息（需要可变内部状态）：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">struct</span> <span class="n">MockMessenger</span> <span class="p">{</span>
    <span class="n">sent_messages</span><span class="p">:</span> <span class="n">RefCell</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;&gt;</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Messenger</span> <span class="k">for</span> <span class="n">MockMessenger</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">send</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.sent_messages</span><span class="nf">.borrow_mut</span><span class="p">()</span><span class="nf">.push</span><span class="p">(</span><span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="n">message</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>外部调用者看到的 <code class="language-plaintext highlighter-rouge">send</code> 接收 <code class="language-plaintext highlighter-rouge">&amp;self</code>，符合 trait 契约；内部通过 <code class="language-plaintext highlighter-rouge">RefCell</code> 完成可变操作。运行时如果出了错（比如两个 <code class="language-plaintext highlighter-rouge">borrow_mut</code> 重叠），测试会 panic，而不是带着 bug 上线。</p>

<h3 id="rcrefcell多所有者--可变">Rc&lt;RefCell<T>&gt;：多所有者 + 可变</T></h3>

<p>把两者嵌套，就能得到”多个所有者且都能修改”的数据结构：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">value</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">RefCell</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">5</span><span class="p">));</span>
<span class="k">let</span> <span class="n">a</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nf">Cons</span><span class="p">(</span><span class="nn">Rc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">value</span><span class="p">),</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nb">Nil</span><span class="p">)));</span>
<span class="k">let</span> <span class="n">b</span> <span class="o">=</span> <span class="nf">Cons</span><span class="p">(</span><span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">RefCell</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">6</span><span class="p">)),</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="p">));</span>

<span class="o">*</span><span class="n">value</span><span class="nf">.borrow_mut</span><span class="p">()</span> <span class="o">+=</span> <span class="mi">10</span><span class="p">;</span>  <span class="c1">// a 和 b 共享的 5 变成 15</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="六引用循环与-weak小心互相指带来的泄漏">六、引用循环与 Weak<T>：小心「互相指」带来的泄漏</T></h2>

<p>Rust 不保证完全避免内存泄漏——如果 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 之间形成循环引用，计数永远到不了 0，内存就不会释放：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">a</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nf">Cons</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="nn">RefCell</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nb">Nil</span><span class="p">))));</span>
<span class="k">let</span> <span class="n">b</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nf">Cons</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="nn">RefCell</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">Rc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="p">))));</span>
<span class="c1">// 让 a 指向 b，形成循环</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">link</span><span class="p">)</span> <span class="o">=</span> <span class="n">a</span><span class="nf">.tail</span><span class="p">()</span> <span class="p">{</span>
    <span class="o">*</span><span class="n">link</span><span class="nf">.borrow_mut</span><span class="p">()</span> <span class="o">=</span> <span class="nn">Rc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">b</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// a 和 b 的 strong_count 都是 2，离开作用域后都剩 1，内存泄漏</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>解决方法是 <strong><code class="language-plaintext highlighter-rouge">Weak&lt;T&gt;</code> 弱引用</strong>。<code class="language-plaintext highlighter-rouge">Rc::downgrade</code> 创建弱引用，它<strong>不拥有</strong>数据，只增加 <code class="language-plaintext highlighter-rouge">weak_count</code>。当 <code class="language-plaintext highlighter-rouge">strong_count</code> 降为 0 时，值就被释放，弱引用通过 <code class="language-plaintext highlighter-rouge">upgrade()</code> 返回 <code class="language-plaintext highlighter-rouge">Option&lt;Rc&lt;T&gt;&gt;</code> 来安全访问——如果值还在就返回 <code class="language-plaintext highlighter-rouge">Some</code>，已被释放就返回 <code class="language-plaintext highlighter-rouge">None</code>。</p>

<p>树结构的父节点引用是 <code class="language-plaintext highlighter-rouge">Weak&lt;T&gt;</code> 的典型场景：父节点拥有子节点（<code class="language-plaintext highlighter-rouge">Rc</code> 强引用），子节点知道父节点（<code class="language-plaintext highlighter-rouge">Weak</code> 弱引用）。父节点被丢弃时子节点跟着消失，子节点被丢弃时父节点不受影响。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">struct</span> <span class="n">Node</span> <span class="p">{</span>
    <span class="n">value</span><span class="p">:</span> <span class="nb">i32</span><span class="p">,</span>
    <span class="n">parent</span><span class="p">:</span> <span class="n">RefCell</span><span class="o">&lt;</span><span class="n">Weak</span><span class="o">&lt;</span><span class="n">Node</span><span class="o">&gt;&gt;</span><span class="p">,</span>      <span class="c1">// 弱引用，不拥有父节点</span>
    <span class="n">children</span><span class="p">:</span> <span class="n">RefCell</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">Rc</span><span class="o">&lt;</span><span class="n">Node</span><span class="o">&gt;&gt;&gt;</span><span class="p">,</span> <span class="c1">// 强引用，拥有子节点</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="实践建议">实践建议</h2>

<ol>
  <li><strong>默认用 <code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code> 处理堆分配和递归类型</strong>，它最简单、零开销、不会引入新的借用复杂度。</li>
  <li><strong>需要共享读权限时选 <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code>，需要共享写权限时套 <code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code></strong>，但记住：
    <ul>
      <li><code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> 只读、<code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code> 单线程——两者都不是万能药</li>
      <li><code class="language-plaintext highlighter-rouge">Rc&lt;RefCell&lt;T&gt;&gt;</code> 组合给了灵活性，也给了制造引用循环的能力</li>
    </ul>
  </li>
  <li><strong>有父子双向引用时，子→父用 <code class="language-plaintext highlighter-rouge">Weak&lt;T&gt;</code></strong>，这是避免循环泄漏的标准做法。始终问自己：这个方向的引用是”拥有”还是”知道”？拥有用 <code class="language-plaintext highlighter-rouge">Rc</code>，知道用 <code class="language-plaintext highlighter-rouge">Weak</code>。</li>
</ol>

<p>智能指针的本质，是 Rust 在”编译期保证安全”这条主线上开出的几扇侧门：<code class="language-plaintext highlighter-rouge">Box</code> 解决大小未知，<code class="language-plaintext highlighter-rouge">Rc</code> 扩展所有权语义，<code class="language-plaintext highlighter-rouge">RefCell</code> 把检查推迟到运行时，<code class="language-plaintext highlighter-rouge">Weak</code> 打破循环。它们都封装在安全的 API 里，底线没有破——只是换了一种方式守住它。下一章，我们把这些工具带进并发世界，看看 Rust 如何用所有权系统实现”无畏并发”。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="Rust" /><category term="学习笔记" /><category term="TRPL" /><summary type="html"><![CDATA[Rust 学习笔记（16/21）：智能指针——当编译期规则需要一点「弹性」]]></summary></entry><entry><title type="html">Rust 学习笔记（15/21）：Cargo 进阶与 Crates.io——从「能跑」到「能被别人用」</title><link href="https://0end1.github.io/2026/09/14/rust-cargo-and-crates-io/" rel="alternate" type="text/html" title="Rust 学习笔记（15/21）：Cargo 进阶与 Crates.io——从「能跑」到「能被别人用」" /><published>2026-09-14T09:00:00+08:00</published><updated>2026-09-14T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/14/rust-cargo-and-crates-io</id><content type="html" xml:base="https://0end1.github.io/2026/09/14/rust-cargo-and-crates-io/"><![CDATA[<h1 id="rust-学习笔记1521cargo-进阶与-cratesio从能跑到能被别人用">Rust 学习笔记（15/21）：Cargo 进阶与 Crates.io——从”能跑”到”能被别人用”</h1>

<blockquote>
  <p>本系列基于官方《Rust 程序设计语言》（TRPL）逐章学习。前几章我们一直在写代码本身：所有权、测试、迭代器。这一章换个视角，把 Cargo 当作真正的工程工具来用——定制构建配置、写文档注释、把 crate 发布到 crates.io，以及用工作空间管理一组协同开发的包。一句话：从”我自己能跑”走向”别人能用、团队能一起维护”。</p>
</blockquote>

<h2 id="开篇同一份代码两副面孔">开篇：同一份代码，两副面孔</h2>

<p><code class="language-plaintext highlighter-rouge">cargo build</code> 和 <code class="language-plaintext highlighter-rouge">cargo build --release</code> 的输出你多半见过：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
$ cargo build --release
    Finished release [optimized] target(s) in 0.0 secs
</pre></td></tr></tbody></table></code></pre></div></div>

<p>方括号里的 <code class="language-plaintext highlighter-rouge">unoptimized + debuginfo</code> 与 <code class="language-plaintext highlighter-rouge">optimized</code>，就是两套<strong>发布配置</strong>（release profiles）的差异。它们不是”两个 Cargo”，而是同一条编译流水线的两组参数：调试时希望编译快、能打断点；发布时希望程序快、体积小，编译久一点无所谓。理解了这一点，本章几乎所有”发布相关”的设计就都好懂了——<strong>在不同阶段付出不同的代价</strong>。</p>

<h2 id="一发布配置把编译参数写进-cargotoml">一、发布配置：把编译参数写进 Cargo.toml</h2>

<p>Cargo 有 <code class="language-plaintext highlighter-rouge">dev</code> 与 <code class="language-plaintext highlighter-rouge">release</code> 两个主要配置，分别对应 <code class="language-plaintext highlighter-rouge">cargo build</code> 与 <code class="language-plaintext highlighter-rouge">cargo build --release</code>。项目里没有 <code class="language-plaintext highlighter-rouge">[profile.*]</code> 时全部用默认值；一旦写上，只覆盖你写的那部分：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="nn">[profile.dev]</span>
<span class="py">opt-level</span> <span class="p">=</span> <span class="mi">0</span>

<span class="nn">[profile.release]</span>
<span class="py">opt-level</span> <span class="p">=</span> <span class="mi">3</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">opt-level</code> 取 0~3，越高优化越多、编译越慢。开发期频繁编译，所以 <code class="language-plaintext highlighter-rouge">dev</code> 默认 0；发布只编译一次却要运行很多次，所以 <code class="language-plaintext highlighter-rouge">release</code> 默认 3。想在开发时多要一点性能，改成 1 即可——不必自己发明一套构建流程。</p>

<table>
  <thead>
    <tr>
      <th>配置</th>
      <th>触发方式</th>
      <th>opt-level 默认</th>
      <th>取舍</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dev</code></td>
      <td><code class="language-plaintext highlighter-rouge">cargo build</code></td>
      <td>0</td>
      <td>编译快、便于调试</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">release</code></td>
      <td><code class="language-plaintext highlighter-rouge">cargo build --release</code></td>
      <td>3</td>
      <td>运行快、编译慢</td>
    </tr>
  </tbody>
</table>

<h2 id="二文档注释写给使用者而非编译器">二、文档注释：写给”使用者”而非编译器</h2>

<p><code class="language-plaintext highlighter-rouge">//</code> 是给读源码的人看的，<code class="language-plaintext highlighter-rouge">///</code> 则是<strong>文档注释</strong>（documentation comments）：支持 Markdown，由 <code class="language-plaintext highlighter-rouge">rustdoc</code> 渲染成 HTML，用来告诉别人如何<strong>使用</strong>你的 crate，而不是它如何被<strong>实现</strong>。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="cd">/// Adds one to the number given.</span>
<span class="cd">///</span>
<span class="cd">/// # Examples</span>
<span class="cd">///</span>
<span class="cd">/// ```</span>
<span class="cd">/// let arg = 5;</span>
<span class="cd">/// let answer = my_crate::add_one(arg);</span>
<span class="cd">/// assert_eq!(6, answer);</span>
<span class="cd">/// ```</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">add_one</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">i32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i32</span> <span class="p">{</span>
    <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>运行 <code class="language-plaintext highlighter-rouge">cargo doc</code> 会在 <code class="language-plaintext highlighter-rouge">target/doc</code> 生成 HTML；<code class="language-plaintext highlighter-rouge">cargo doc --open</code> 直接构建并打开浏览器（连同所有依赖的文档）。</p>

<p>除了 <code class="language-plaintext highlighter-rouge"># Examples</code>，社区常用的小节还有三个，本质是一份”提醒你去检查”的清单：</p>

<table>
  <thead>
    <tr>
      <th>小节</th>
      <th>何时写</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge"># Examples</code></td>
      <td>几乎总是值得写，最直观</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge"># Panics</code></td>
      <td>函数可能 panic 的场景</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge"># Errors</code></td>
      <td>返回 <code class="language-plaintext highlighter-rouge">Result</code> 时，何种情况返回 <code class="language-plaintext highlighter-rouge">Err</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge"># Safety</code></td>
      <td><code class="language-plaintext highlighter-rouge">unsafe</code> 函数对调用者的前置要求</td>
    </tr>
  </tbody>
</table>

<p>文档注释最妙的一点是<strong>文档即测试</strong>：<code class="language-plaintext highlighter-rouge">cargo test</code> 会把文档里的示例代码当测试运行：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>   Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
</pre></td></tr></tbody></table></code></pre></div></div>

<p>如果你改了函数却忘了改例子，<code class="language-plaintext highlighter-rouge">assert_eq!</code> 就会失败——文档从此不会”过期”。</p>

<h2 id="三-与-pub-use让使用者少走几步">三、<code class="language-plaintext highlighter-rouge">//!</code> 与 <code class="language-plaintext highlighter-rouge">pub use</code>：让使用者少走几步</h2>

<p><code class="language-plaintext highlighter-rouge">///</code> 描述它<strong>之后</strong>的项；<code class="language-plaintext highlighter-rouge">//!</code> 描述<strong>包含它的项</strong>，因此常写在 <code class="language-plaintext highlighter-rouge">src/lib.rs</code> 或模块根部，为整个 crate 或模块写总览：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="cd">//! # My Crate</span>
<span class="cd">//!</span>
<span class="cd">//! `my_crate` is a collection of utilities to make performing certain</span>
<span class="cd">//! calculations more convenient.</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>它必须放在文件最顶部（”最后一行之后没有任何代码”恰恰说明它属于这个文件本身）。</p>

<p><code class="language-plaintext highlighter-rouge">pub use</code> 解决另一个问题：<strong>你的文件结构往往不等于用户想要的 API 结构</strong>。假设 <code class="language-plaintext highlighter-rouge">art</code> 库把内容拆成 <code class="language-plaintext highlighter-rouge">kinds</code> 与 <code class="language-plaintext highlighter-rouge">utils</code> 两个模块，用户必须写：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">use</span> <span class="nn">art</span><span class="p">::</span><span class="nn">kinds</span><span class="p">::</span><span class="n">PrimaryColor</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">art</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="n">mix</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>作者只需在 <code class="language-plaintext highlighter-rouge">lib.rs</code> 顶部加三行重导出（re-export）：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">use</span> <span class="k">self</span><span class="p">::</span><span class="nn">kinds</span><span class="p">::</span><span class="n">PrimaryColor</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">use</span> <span class="k">self</span><span class="p">::</span><span class="nn">kinds</span><span class="p">::</span><span class="n">SecondaryColor</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">use</span> <span class="k">self</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="n">mix</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>用户就能写成 <code class="language-plaintext highlighter-rouge">use art::PrimaryColor;</code>。<code class="language-plaintext highlighter-rouge">pub use</code> 让<strong>内部组织</strong>与<strong>对外 API</strong> 解耦：内部维持清晰分层，对外提供扁平入口，<code class="language-plaintext highlighter-rouge">cargo doc</code> 首页也会直接列出这些重导出项。原有的深层路径依然可用，只是不再强迫用户走那么深。</p>

<h2 id="四发布到-cratesio把代码交到别人手里">四、发布到 crates.io：把代码交到别人手里</h2>

<p>发布不是一条命令，而是一串有先后顺序的准备：</p>

<table>
  <thead>
    <tr>
      <th>步骤</th>
      <th>命令/动作</th>
      <th>要点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1. 注册账号</td>
      <td>用 GitHub 登录 crates.io，在账户页获取 API token</td>
      <td>token 是秘密，泄露要立刻重新生成</td>
    </tr>
    <tr>
      <td>2. 登录本地 Cargo</td>
      <td><code class="language-plaintext highlighter-rouge">cargo login &lt;token&gt;</code></td>
      <td>存入 <code class="language-plaintext highlighter-rouge">~/.cargo/credentials</code></td>
    </tr>
    <tr>
      <td>3. 补全元信息</td>
      <td>编辑 <code class="language-plaintext highlighter-rouge">Cargo.toml</code> 的 <code class="language-plaintext highlighter-rouge">[package]</code></td>
      <td>名称唯一（先到先得）、<code class="language-plaintext highlighter-rouge">description</code>、<code class="language-plaintext highlighter-rouge">license</code></td>
    </tr>
    <tr>
      <td>4. 发布</td>
      <td><code class="language-plaintext highlighter-rouge">cargo publish</code></td>
      <td>先在本地打包并编译验证</td>
    </tr>
  </tbody>
</table>

<p>缺 <code class="language-plaintext highlighter-rouge">description</code> 或 <code class="language-plaintext highlighter-rouge">license</code> 时，<code class="language-plaintext highlighter-rouge">cargo publish</code> 会直接报 <code class="language-plaintext highlighter-rouge">error: api errors: missing or empty metadata fields: description, license.</code>。补齐后大致是这样：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nn">[package]</span>
<span class="py">name</span> <span class="p">=</span> <span class="s">"guessing_game"</span>
<span class="py">version</span> <span class="p">=</span> <span class="s">"0.1.0"</span>
<span class="py">authors</span> <span class="p">=</span> <span class="p">[</span><span class="s">"Your Name &lt;you@example.com&gt;"</span><span class="p">]</span>
<span class="py">edition</span> <span class="p">=</span> <span class="s">"2018"</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">"A fun game where you guess what number the computer has chosen."</span>
<span class="py">license</span> <span class="p">=</span> <span class="s">"MIT OR Apache-2.0"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">license</code> 用 SPDX 标识符；找不到合适的就用 <code class="language-plaintext highlighter-rouge">license-file</code> 指向许可证文件。许多 Rust 项目选择 <code class="language-plaintext highlighter-rouge">MIT OR Apache-2.0</code> 双许可（<code class="language-plaintext highlighter-rouge">OR</code> 分隔多个标识符）。</p>

<p>两条必须记住的规则：</p>

<ul>
  <li><strong>发布是永久性的</strong>：版本号一旦发布，不能覆盖、不能删除代码。crates.io 要做”永久文档服务器”，好让所有依赖者的构建永远可复现；想改就升版本号（遵循语义化版本）再发布一次。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">cargo yank</code> 只是撤回，不是删除</strong>：<code class="language-plaintext highlighter-rouge">cargo yank --vers 1.0.1</code> 让新项目不再选中该版本，但已有 <code class="language-plaintext highlighter-rouge">Cargo.lock</code> 的项目照旧能下载，不会被”断供”，加 <code class="language-plaintext highlighter-rouge">--undo</code> 可撤销撤回。它<strong>不是</strong>用来删除误传密钥的——那种情况请立刻重置密钥。</li>
</ul>

<h2 id="五工作空间让一组-crate-一起长大">五、工作空间：让一组 crate 一起长大</h2>

<p>项目变大时，”一个大库”往往该拆成几个协同的小 crate。Cargo 的<strong>工作空间</strong>（workspaces）正是为这种场景准备：一组共享同一个 <code class="language-plaintext highlighter-rouge">Cargo.lock</code> 和同一个 <code class="language-plaintext highlighter-rouge">target</code> 目录的包。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre>├── Cargo.toml      ← 只写 [workspace]，没有 [package]
├── Cargo.lock      ← 全工作空间唯一
├── add-one/        ← 库 crate
│   ├── Cargo.toml
│   └── src/lib.rs
├── adder/          ← 二进制 crate
│   ├── Cargo.toml
│   └── src/main.rs
└── target/         ← 全工作空间唯一
</pre></td></tr></tbody></table></code></pre></div></div>

<p>根 <code class="language-plaintext highlighter-rouge">Cargo.toml</code> 只负责登记成员：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nn">[workspace]</span>

<span class="py">members</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s">"adder"</span><span class="p">,</span>
    <span class="s">"add-one"</span><span class="p">,</span>
<span class="p">]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">adder</code> 要用 <code class="language-plaintext highlighter-rouge">add-one</code>，需显式声明<strong>路径依赖</strong>（Cargo 不假定工作空间内的 crate 互相依赖）：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nn">[dependencies]</span>
<span class="nn">add-one</span> <span class="o">=</span> <span class="p">{</span> <span class="py">path</span> <span class="p">=</span> <span class="s">"../add-one"</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>关注点</th>
      <th>工作空间下的行为</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>构建产物</td>
      <td>统一放在根 <code class="language-plaintext highlighter-rouge">target/</code>，避免各 crate 重复编译</td>
    </tr>
    <tr>
      <td>依赖版本</td>
      <td>唯一的 <code class="language-plaintext highlighter-rouge">Cargo.lock</code>，保证所有成员用同一版本、彼此兼容</td>
    </tr>
    <tr>
      <td>使用依赖</td>
      <td>每个要用的 crate 仍需在自己 <code class="language-plaintext highlighter-rouge">Cargo.toml</code> 中声明（但不会重复下载）</td>
    </tr>
    <tr>
      <td>运行/测试</td>
      <td><code class="language-plaintext highlighter-rouge">cargo run -p adder</code>、<code class="language-plaintext highlighter-rouge">cargo test</code>（全部）、<code class="language-plaintext highlighter-rouge">cargo test -p add-one</code>（单个）</td>
    </tr>
    <tr>
      <td>发布</td>
      <td>没有 <code class="language-plaintext highlighter-rouge">--all</code>/<code class="language-plaintext highlighter-rouge">-p</code>，只能进入每个 crate 目录逐个 <code class="language-plaintext highlighter-rouge">cargo publish</code></td>
    </tr>
  </tbody>
</table>

<p>一个容易踩的坑是<strong>外部依赖</strong>：假设 <code class="language-plaintext highlighter-rouge">add-one</code> 在 <code class="language-plaintext highlighter-rouge">Cargo.toml</code> 里加了 <code class="language-plaintext highlighter-rouge">rand = "0.5.5"</code>，根目录 <code class="language-plaintext highlighter-rouge">Cargo.lock</code> 会记录它；但若 <code class="language-plaintext highlighter-rouge">adder</code> 也写 <code class="language-plaintext highlighter-rouge">use rand;</code>，编译就会报错——<code class="language-plaintext highlighter-rouge">rand</code> 并未成为 <code class="language-plaintext highlighter-rouge">adder</code> 的依赖。Cargo 不做”依赖传染”：每个 crate 想用什么，都要在自己的 <code class="language-plaintext highlighter-rouge">Cargo.toml</code> 里声明。好在声明之后不会下载第二份拷贝，所有成员共享同一版本。测试同理：根目录 <code class="language-plaintext highlighter-rouge">cargo test</code> 跑遍所有成员，<code class="language-plaintext highlighter-rouge">cargo test -p add-one</code> 只跑指定 crate。</p>

<p>共享 <code class="language-plaintext highlighter-rouge">target</code> 与 <code class="language-plaintext highlighter-rouge">Cargo.lock</code> 是最实际的两个收益：既省磁盘、省编译时间，也从根上消除了”同一依赖装了两个版本”的不兼容麻烦。</p>

<h2 id="六装别人的工具扩展自己的能力">六、装别人的工具、扩展自己的能力</h2>

<p><code class="language-plaintext highlighter-rouge">cargo install</code> 用来在本地安装<strong>二进制</strong> crate（只有含 <code class="language-plaintext highlighter-rouge">src/main.rs</code> 这类二进制目标的包能装），比如第 12 章提到的 <code class="language-plaintext highlighter-rouge">ripgrep</code>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>$ cargo install ripgrep
  Installing ~/.cargo/bin/rg
</pre></td></tr></tbody></table></code></pre></div></div>

<p>它装到 Rust 根目录的 <code class="language-plaintext highlighter-rouge">bin</code>（rustup 默认是 <code class="language-plaintext highlighter-rouge">$HOME/.cargo/bin</code>），把这个目录放进 <code class="language-plaintext highlighter-rouge">$PATH</code> 就能直接用。其定位是”方便 Rust 开发者安装社区工具”，而非替代系统包管理器。</p>

<p>Cargo 还能被<strong>扩展</strong>：只要 <code class="language-plaintext highlighter-rouge">$PATH</code> 里有名为 <code class="language-plaintext highlighter-rouge">cargo-something</code> 的可执行文件，就能用 <code class="language-plaintext highlighter-rouge">cargo something</code> 调用它，甚至会被 <code class="language-plaintext highlighter-rouge">cargo --list</code> 列出。于是”安装第三方 cargo 子命令”的体验与内建命令几乎一致。</p>

<h2 id="实践建议与总结">实践建议与总结</h2>

<ol>
  <li><strong>配置是分阶段的</strong>：<code class="language-plaintext highlighter-rouge">dev</code>/<code class="language-plaintext highlighter-rouge">release</code> 的差异提醒我们，开发期优先反馈速度、发布期优先运行质量；<code class="language-plaintext highlighter-rouge">opt-level</code> 是唯一常见的手调旋钮，别急着全局优化。</li>
  <li><strong>文档要写成”可执行的承诺”</strong>：<code class="language-plaintext highlighter-rouge">///</code> + <code class="language-plaintext highlighter-rouge"># Examples</code> 让 <code class="language-plaintext highlighter-rouge">cargo test</code> 替你检查示例是否过期，<code class="language-plaintext highlighter-rouge">//!</code> 给出 crate 总览，<code class="language-plaintext highlighter-rouge">pub use</code> 把好用的入口顶到台前——三者合起来才是”别人能用”的文档。</li>
  <li><strong>先想清楚公开边界再发布</strong>：名称唯一、版本永久、依赖锁版本是硬约束；发布前补齐 <code class="language-plaintext highlighter-rouge">description</code>/<code class="language-plaintext highlighter-rouge">license</code>，出事后记住 <code class="language-plaintext highlighter-rouge">yank</code> 是”止损”而非”删除”。</li>
</ol>

<p>回到开头那句：Cargo 的进阶功能几乎都在回答同一个问题——<strong>这份代码要交给别人（或未来的自己）吗？</strong> 要交给别人，就该有文档、有明确的公开 API、有可复现的依赖与版本。下一章我们重新回到语言本身，看 <code class="language-plaintext highlighter-rouge">Box&lt;T&gt;</code>、<code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code>、<code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code> 这些”智能指针”如何在编译期规则之外，为数据所有权提供更多可能。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="Rust" /><category term="学习笔记" /><category term="TRPL" /><summary type="html"><![CDATA[Rust 学习笔记（15/21）：Cargo 进阶与 Crates.io——从”能跑”到”能被别人用”]]></summary></entry><entry><title type="html">Rust 学习笔记（14/21）：迭代器与闭包——Rust 的「零成本抽象」是怎么做到的</title><link href="https://0end1.github.io/2026/09/13/rust-iterators-and-closures/" rel="alternate" type="text/html" title="Rust 学习笔记（14/21）：迭代器与闭包——Rust 的「零成本抽象」是怎么做到的" /><published>2026-09-13T09:00:00+08:00</published><updated>2026-09-13T09:00:00+08:00</updated><id>https://0end1.github.io/2026/09/13/rust-iterators-and-closures</id><content type="html" xml:base="https://0end1.github.io/2026/09/13/rust-iterators-and-closures/"><![CDATA[<h1 id="rust-学习笔记1421迭代器与闭包rust-的零成本抽象是怎么做到的">Rust 学习笔记（14/21）：迭代器与闭包——Rust 的”零成本抽象”是怎么做到的</h1>

<blockquote>
  <p>本系列基于官方《Rust 程序设计语言》（TRPL）逐章学习。前面我们一直在”写得对”的层面打磨：所有权、生命周期、测试。这一章转向”写得巧”——Rust 从函数式语言借来的两大件：<strong>闭包</strong>和<strong>迭代器</strong>。它们让代码看起来更高级，却几乎不付运行时性能代价。这一章还顺手把上一章的 <code class="language-plaintext highlighter-rouge">minigrep</code> 用迭代器重写了一遍，作为”抽象到底值不值”的实证。</p>
</blockquote>

<h2 id="开篇一个健身-app-的两难">开篇：一个健身 App 的两难</h2>

<p>设想你在一个生成定制健身计划的初创公司，后端用 Rust 写。核心算法要考虑年龄、BMI、喜好、近期活动量……计算一次大约两秒，所以我们用一个 <code class="language-plaintext highlighter-rouge">simulated_expensive_calculation</code> 函数模拟它（打印 <code class="language-plaintext highlighter-rouge">calculating slowly...</code>、睡 2 秒、返回传入的数字）。</p>

<p>业务逻辑 <code class="language-plaintext highlighter-rouge">generate_workout(intensity, random_number)</code> 是：低强度（<code class="language-plaintext highlighter-rouge">intensity &lt; 25</code>）建议做俯卧撑和仰卧起坐；高强度时随机数为 3 就休息，否则跑步若干分钟。慢计算因此被调用三处——第一个 <code class="language-plaintext highlighter-rouge">if</code> 分支调了<strong>两次</strong>（用户白等一倍），<code class="language-plaintext highlighter-rouge">else</code> 内的 <code class="language-plaintext highlighter-rouge">if</code> 分支<strong>不该调</strong>，最后再调<strong>一次</strong>。</p>

<p>第一次重构是把结果提取到变量 <code class="language-plaintext highlighter-rouge">expensive_result</code>：调用统一了，代价却是<strong>所有情况都得先等两秒</strong>，包括不需要结果的分支。我们要的是——<strong>在一处定义代码，只在需要时才执行</strong>。这正是闭包登场的地方：像把菜谱写进信封揣兜里，写下来不算做菜，饿了拆开照做，且只做一次。</p>

<h2 id="闭包能捕获环境的匿名函数">闭包：能捕获环境的匿名函数</h2>

<p>闭包是”可存进变量、或作为参数传给其他函数的匿名函数”。语法从一对<strong>竖线</strong>开始：<code class="language-plaintext highlighter-rouge">|num| { ... }</code>，多参数用逗号分隔，只有一行时大括号可省，体最后一行即返回值。这条 <code class="language-plaintext highlighter-rouge">let</code> 存的是<strong>定义</strong>而非调用结果——代码躺在变量里，等你用 <code class="language-plaintext highlighter-rouge">expensive_closure(intensity)</code> 触发。</p>

<h3 id="类型推断与标注">类型推断与标注</h3>

<p>闭包不要求标注参数与返回类型：它存在变量里、匿名、不供库用户调用，且通常很短、上下文很窄，编译器能可靠推断，强制标注只会重复编译器已知的信息。想写也行，四种等价写法：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">fn</span>  <span class="nf">add_one_v1</span>   <span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">u32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">u32</span> <span class="p">{</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">}</span>
<span class="k">let</span> <span class="n">add_one_v2</span> <span class="o">=</span> <span class="p">|</span><span class="n">x</span><span class="p">:</span> <span class="nb">u32</span><span class="p">|</span> <span class="k">-&gt;</span> <span class="nb">u32</span> <span class="p">{</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">};</span>
<span class="k">let</span> <span class="n">add_one_v3</span> <span class="o">=</span> <span class="p">|</span><span class="n">x</span><span class="p">|</span>             <span class="p">{</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">};</span>
<span class="k">let</span> <span class="n">add_one_v4</span> <span class="o">=</span> <span class="p">|</span><span class="n">x</span><span class="p">|</span>               <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span>  <span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>代价是：<strong>闭包的具体类型会被第一次调用锁定</strong>。下面这段先传 <code class="language-plaintext highlighter-rouge">String</code> 再传整数，编译器会报 <code class="language-plaintext highlighter-rouge">E0308: mismatched types</code>，因为 <code class="language-plaintext highlighter-rouge">x</code> 与返回值已被推断并锁定为 <code class="language-plaintext highlighter-rouge">String</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">example_closure</span> <span class="o">=</span> <span class="p">|</span><span class="n">x</span><span class="p">|</span> <span class="n">x</span><span class="p">;</span>
<span class="k">let</span> <span class="n">s</span> <span class="o">=</span> <span class="nf">example_closure</span><span class="p">(</span><span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">"hello"</span><span class="p">));</span>
<span class="k">let</span> <span class="n">n</span> <span class="o">=</span> <span class="nf">example_closure</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>   <span class="c1">// error[E0308]: expected String, found integer</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="用-fn-trait-把闭包装进结构体">用 Fn trait 把闭包装进结构体</h3>

<p>要解决”同一结果被算两次”，除了到处存变量，还有更优雅的方案：<strong>用一个结构体存放闭包并缓存结果</strong>——这个模式叫 <em>memoization</em> 或<strong>惰性求值</strong>（lazy evaluation）。难点在于结构体字段必须有类型，而每个闭包实例都有自己<strong>独有的匿名类型</strong>（即使签名相同也互不相同），所以要用泛型 + trait bound。标准库提供的三个 <code class="language-plaintext highlighter-rouge">Fn</code> 系列 trait 中，这里用 <code class="language-plaintext highlighter-rouge">Fn</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="k">struct</span> <span class="n">Cacher</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span>
<span class="k">where</span>
    <span class="n">T</span><span class="p">:</span> <span class="nf">Fn</span><span class="p">(</span><span class="nb">u32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">u32</span><span class="p">,</span>
<span class="p">{</span>
    <span class="n">calculation</span><span class="p">:</span> <span class="n">T</span><span class="p">,</span>
    <span class="n">value</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">u32</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">Cacher</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span>
<span class="k">where</span>
    <span class="n">T</span><span class="p">:</span> <span class="nf">Fn</span><span class="p">(</span><span class="nb">u32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">u32</span><span class="p">,</span>
<span class="p">{</span>
    <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">calculation</span><span class="p">:</span> <span class="n">T</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Cacher</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="n">Cacher</span> <span class="p">{</span> <span class="n">calculation</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">None</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">value</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">arg</span><span class="p">:</span> <span class="nb">u32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">u32</span> <span class="p">{</span>
        <span class="k">match</span> <span class="k">self</span><span class="py">.value</span> <span class="p">{</span>
            <span class="nf">Some</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">v</span><span class="p">,</span>
            <span class="nb">None</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="k">let</span> <span class="n">v</span> <span class="o">=</span> <span class="p">(</span><span class="k">self</span><span class="py">.calculation</span><span class="p">)(</span><span class="n">arg</span><span class="p">);</span>
                <span class="k">self</span><span class="py">.value</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="n">v</span><span class="p">);</span>
                <span class="n">v</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>字段私有，是为了让 <code class="language-plaintext highlighter-rouge">Cacher</code> 自己管理缓存而不被外部乱改。调用方把闭包交给 <code class="language-plaintext highlighter-rouge">Cacher::new</code>，之后一律走 <code class="language-plaintext highlighter-rouge">value(intensity)</code>：有缓存直接返回，没有才执行并存入。于是慢计算<strong>最多只跑一次</strong>，<code class="language-plaintext highlighter-rouge">generate_workout</code> 得以专注业务逻辑。（顺带一提：函数也实现了这三个 <code class="language-plaintext highlighter-rouge">Fn</code> trait，不需要捕获环境时直接传函数即可。）</p>

<p>但这个 <code class="language-plaintext highlighter-rouge">Cacher</code> 有两个限制，导致难以复用：</p>

<table>
  <thead>
    <tr>
      <th>限制</th>
      <th>现象</th>
      <th>改进方向</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>假设”同一 arg 总返回同一值”</td>
      <td>测试 <code class="language-plaintext highlighter-rouge">call_with_different_values</code> 失败：先 <code class="language-plaintext highlighter-rouge">value(1)</code> 再 <code class="language-plaintext highlighter-rouge">value(2)</code>，缓存里是 <code class="language-plaintext highlighter-rouge">Some(1)</code>，断言报 <code class="language-plaintext highlighter-rouge">left: 1, right: 2</code></td>
      <td>用 <code class="language-plaintext highlighter-rouge">HashMap</code> 以 <code class="language-plaintext highlighter-rouge">arg</code> 为 key 缓存多组结果</td>
    </tr>
    <tr>
      <td>被写死为 <code class="language-plaintext highlighter-rouge">u32 -&gt; u32</code></td>
      <td>想缓存 <code class="language-plaintext highlighter-rouge">&amp;str -&gt; usize</code> 的闭包就不能用</td>
      <td>引入更多泛型参数提高灵活性</td>
    </tr>
  </tbody>
</table>

<h3 id="捕获环境与三种-fn-trait">捕获环境与三种 Fn trait</h3>

<p>闭包还有一个函数没有的能力：<strong>捕获其定义作用域中的变量</strong>。</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="k">let</span> <span class="n">equal_to_x</span> <span class="o">=</span> <span class="p">|</span><span class="n">z</span><span class="p">|</span> <span class="n">z</span> <span class="o">==</span> <span class="n">x</span><span class="p">;</span>   <span class="c1">// 闭包体内用了 x，尽管 x 不是参数</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>写成 <code class="language-plaintext highlighter-rouge">fn equal_to_x(z: i32) -&gt; bool { z == x }</code> 会编译失败：<code class="language-plaintext highlighter-rouge">E0434: can't capture dynamic environment in a fn item</code>。代价是闭包要为捕获的变量占内存，函数则从不需要。捕获方式恰好对应函数取参的三种方式：</p>

<table>
  <thead>
    <tr>
      <th>trait</th>
      <th>捕获方式</th>
      <th>对应函数参数</th>
      <th>何时实现</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FnOnce</code></td>
      <td>取得所有权</td>
      <td>按值传参</td>
      <td><strong>所有</strong>闭包都实现（因为至少要被调用一次）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FnMut</code></td>
      <td>可变借用</td>
      <td><code class="language-plaintext highlighter-rouge">&amp;mut</code></td>
      <td>没有把捕获变量所有权移进闭包的闭包</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Fn</code></td>
      <td>不可变借用</td>
      <td><code class="language-plaintext highlighter-rouge">&amp;</code></td>
      <td>不需要对被捕获变量可变访问的闭包</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">equal_to_x</code> 只读取 <code class="language-plaintext highlighter-rouge">x</code>，因此实现 <code class="language-plaintext highlighter-rouge">Fn</code>。若在参数列表前加 <code class="language-plaintext highlighter-rouge">move</code> 关键字，就强制闭包取得环境值的所有权——这在<strong>把闭包传给新线程</strong>时最实用（第 16 章详讲）。演示一下 <code class="language-plaintext highlighter-rouge">move</code> 的后果，这次用 <code class="language-plaintext highlighter-rouge">Vec</code> 而非整数（整数可拷贝、不会真的移走）：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">x</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">];</span>
<span class="k">let</span> <span class="n">equal_to_x</span> <span class="o">=</span> <span class="k">move</span> <span class="p">|</span><span class="n">z</span><span class="p">|</span> <span class="n">z</span> <span class="o">==</span> <span class="n">x</span><span class="p">;</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"can't use x here: {:?}"</span><span class="p">,</span> <span class="n">x</span><span class="p">);</span>   <span class="c1">// error[E0382]: use of moved value: `x`</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">x</code> 被移进闭包，<code class="language-plaintext highlighter-rouge">main</code> 里就不能再用它了。实用建议：trait bound <strong>先从 <code class="language-plaintext highlighter-rouge">Fn</code> 开始</strong>，编译器会告诉你何时该换成 <code class="language-plaintext highlighter-rouge">FnMut</code> 或 <code class="language-plaintext highlighter-rouge">FnOnce</code>。</p>

<h2 id="迭代器惰性的传送带">迭代器：惰性的传送带</h2>

<p>迭代器负责”遍历每一项 + 决定何时结束”，最大特点是<strong>惰性</strong>：<code class="language-plaintext highlighter-rouge">let v1_iter = v1.iter();</code> 本身什么都不发生，直到被真正消费。核心是标准库的 <code class="language-plaintext highlighter-rouge">Iterator</code> trait：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">trait</span> <span class="nb">Iterator</span> <span class="p">{</span>
    <span class="k">type</span> <span class="n">Item</span><span class="p">;</span>
    <span class="k">fn</span> <span class="nf">next</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">Item</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="c1">// 此处省略了方法的默认实现</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">type Item</code> 是<strong>关联类型</strong>（第 19 章详解），即迭代器产出元素的类型；<code class="language-plaintext highlighter-rouge">next</code> 是唯一必须实现的方法，一次返回一个项包在 <code class="language-plaintext highlighter-rouge">Some</code> 里，结束返回 <code class="language-plaintext highlighter-rouge">None</code>。直接调用 <code class="language-plaintext highlighter-rouge">next</code> 时迭代器变量必须 <code class="language-plaintext highlighter-rouge">mut</code>——它会改变记录位置的状态，也就是<strong>消费</strong>了迭代器（<code class="language-plaintext highlighter-rouge">for</code> 循环会自己取得所有权并内部处理可变性）。三个常用入口方法：</p>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>产出的迭代器</th>
      <th>适用场景</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">iter</code></td>
      <td>元素的不可变引用（<code class="language-plaintext highlighter-rouge">&amp;T</code>）</td>
      <td>只读遍历（如 <code class="language-plaintext highlighter-rouge">search</code>）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">iter_mut</code></td>
      <td>元素的可变引用（<code class="language-plaintext highlighter-rouge">&amp;mut T</code>）</td>
      <td>需要原地修改元素</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">into_iter</code></td>
      <td>拥有所有权的元素（<code class="language-plaintext highlighter-rouge">T</code>）</td>
      <td>需要搬走元素（如 <code class="language-plaintext highlighter-rouge">shoes_in_my_size</code>）</td>
    </tr>
  </tbody>
</table>

<h3 id="消费适配器-vs-迭代器适配器">消费适配器 vs 迭代器适配器</h3>

<p><code class="language-plaintext highlighter-rouge">Iterator</code> trait 的众多方法分两类，区别十分关键：</p>

<table>
  <thead>
    <tr>
      <th>类别</th>
      <th>代表方法</th>
      <th>是否取得所有权</th>
      <th>是否立即执行</th>
      <th>备注</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>消费适配器</strong></td>
      <td><code class="language-plaintext highlighter-rouge">sum</code>、<code class="language-plaintext highlighter-rouge">collect</code></td>
      <td>是（消费后迭代器不可再用）</td>
      <td>是</td>
      <td>内部反复调用 <code class="language-plaintext highlighter-rouge">next</code> 直到结束</td>
    </tr>
    <tr>
      <td><strong>迭代器适配器</strong></td>
      <td><code class="language-plaintext highlighter-rouge">map</code>、<code class="language-plaintext highlighter-rouge">filter</code>、<code class="language-plaintext highlighter-rouge">zip</code>、<code class="language-plaintext highlighter-rouge">skip</code></td>
      <td>否</td>
      <td>否，惰性</td>
      <td>返回新迭代器，可链式调用</td>
    </tr>
  </tbody>
</table>

<p>经典陷阱是只写 <code class="language-plaintext highlighter-rouge">v1.iter().map(|x| x + 1);</code> 而不消费它——编译器会警告 <code class="language-plaintext highlighter-rouge">unused std::iter::Map ... iterator adaptors are lazy and do nothing unless consumed</code>，那个闭包<strong>从未被调用过</strong>。补上 <code class="language-plaintext highlighter-rouge">collect</code> 才是完整用法：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">v2</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">_</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">v1</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">x</span><span class="p">|</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span><span class="nf">.collect</span><span class="p">();</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="n">v2</span><span class="p">,</span> <span class="nd">vec!</span><span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">filter</code> 则常与捕获环境的闭包搭配——下面这个函数只挑出指定尺码的鞋子，闭包捕获了 <code class="language-plaintext highlighter-rouge">shoe_size</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">shoes_in_my_size</span><span class="p">(</span><span class="n">shoes</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Shoe</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">shoe_size</span><span class="p">:</span> <span class="nb">u32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Shoe</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="n">shoes</span><span class="nf">.into_iter</span><span class="p">()</span>
        <span class="nf">.filter</span><span class="p">(|</span><span class="n">s</span><span class="p">|</span> <span class="n">s</span><span class="py">.size</span> <span class="o">==</span> <span class="n">shoe_size</span><span class="p">)</span>
        <span class="nf">.collect</span><span class="p">()</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>因为 <code class="language-plaintext highlighter-rouge">map</code> 接受闭包，我们可以自定义”每个元素要做什么”，同时复用 <code class="language-plaintext highlighter-rouge">Iterator</code> 提供的迭代逻辑——这是闭包与迭代器配合的绝佳示例。</p>

<h3 id="自定义迭代器只需要实现-next">自定义迭代器：只需要实现 next</h3>

<p>只要为类型实现 <code class="language-plaintext highlighter-rouge">Iterator</code>，就能免费获得标准库所有默认方法。下面这个 <code class="language-plaintext highlighter-rouge">Counter</code> 从 1 数到 5：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">impl</span> <span class="nb">Iterator</span> <span class="k">for</span> <span class="n">Counter</span> <span class="p">{</span>
    <span class="k">type</span> <span class="n">Item</span> <span class="o">=</span> <span class="nb">u32</span><span class="p">;</span>

    <span class="k">fn</span> <span class="nf">next</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">::</span><span class="n">Item</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.count</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="k">if</span> <span class="k">self</span><span class="py">.count</span> <span class="o">&lt;</span> <span class="mi">6</span> <span class="p">{</span>
            <span class="nf">Some</span><span class="p">(</span><span class="k">self</span><span class="py">.count</span><span class="p">)</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nb">None</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>有了 <code class="language-plaintext highlighter-rouge">next</code>，所有默认方法就都免费到手了。例如把两个 <code class="language-plaintext highlighter-rouge">Counter</code> 配对（第二个 <code class="language-plaintext highlighter-rouge">skip(1)</code> 跳过首值）、相乘、只留能被 3 整除者、再求和：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">sum</span><span class="p">:</span> <span class="nb">u32</span> <span class="o">=</span> <span class="nn">Counter</span><span class="p">::</span><span class="nf">new</span><span class="p">()</span>
    <span class="nf">.zip</span><span class="p">(</span><span class="nn">Counter</span><span class="p">::</span><span class="nf">new</span><span class="p">()</span><span class="nf">.skip</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
    <span class="nf">.map</span><span class="p">(|(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)|</span> <span class="n">a</span> <span class="o">*</span> <span class="n">b</span><span class="p">)</span>
    <span class="nf">.filter</span><span class="p">(|</span><span class="n">x</span><span class="p">|</span> <span class="n">x</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
    <span class="nf">.sum</span><span class="p">();</span>
<span class="nd">assert_eq!</span><span class="p">(</span><span class="mi">18</span><span class="p">,</span> <span class="n">sum</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>注意 <code class="language-plaintext highlighter-rouge">zip</code> 只产生四对值：理论上第五对 <code class="language-plaintext highlighter-rouge">(5, None)</code> 从未产生，因为 <code class="language-plaintext highlighter-rouge">zip</code> 在任一输入迭代器返回 <code class="language-plaintext highlighter-rouge">None</code> 时就返回 <code class="language-plaintext highlighter-rouge">None</code>。</p>

<h2 id="回头重写-minigrep用迭代器去掉-clone-和可变状态">回头重写 minigrep：用迭代器去掉 clone 和可变状态</h2>

<p>上一章我们留下两处”以后再说”的地方，现在可以收拾了。</p>

<h3 id="一confignew从索引--clone-到移动所有权">一、<code class="language-plaintext highlighter-rouge">Config::new</code>：从索引 + clone 到移动所有权</h3>

<p>原来 <code class="language-plaintext highlighter-rouge">new</code> 接收 <code class="language-plaintext highlighter-rouge">&amp;[String]</code>，自身不拥有参数，为了返回拥有 <code class="language-plaintext highlighter-rouge">query</code>/<code class="language-plaintext highlighter-rouge">filename</code> 的 <code class="language-plaintext highlighter-rouge">Config</code>，只好 <code class="language-plaintext highlighter-rouge">clone</code> 两次。既然 <code class="language-plaintext highlighter-rouge">env::args</code> 本身就返回迭代器，我们干脆把<strong>迭代器的所有权</strong>交给 <code class="language-plaintext highlighter-rouge">Config::new</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">// main.rs：不再 collect 成 Vec，直接传迭代器</span>
<span class="k">let</span> <span class="n">config</span> <span class="o">=</span> <span class="nn">Config</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">env</span><span class="p">::</span><span class="nf">args</span><span class="p">())</span><span class="nf">.unwrap_or_else</span><span class="p">(|</span><span class="n">err</span><span class="p">|</span> <span class="p">{</span>
    <span class="nd">eprintln!</span><span class="p">(</span><span class="s">"Problem parsing arguments: {}"</span><span class="p">,</span> <span class="n">err</span><span class="p">);</span>
    <span class="nn">process</span><span class="p">::</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>签名相应改成 <code class="language-plaintext highlighter-rouge">pub fn new(mut args: std::env::Args) -&gt; Result&lt;Config, &amp;'static str&gt;</code>（<code class="language-plaintext highlighter-rouge">args</code> 要被迭代推进状态，故需 <code class="language-plaintext highlighter-rouge">mut</code>），函数体里的长度检查与索引访问全部换成 <code class="language-plaintext highlighter-rouge">next</code>：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="k">impl</span> <span class="n">Config</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="k">mut</span> <span class="n">args</span><span class="p">:</span> <span class="nn">std</span><span class="p">::</span><span class="nn">env</span><span class="p">::</span><span class="n">Args</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Config</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">'static</span> <span class="nb">str</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="n">args</span><span class="nf">.next</span><span class="p">();</span>                    <span class="c1">// 第一个值是程序名，跳过</span>

        <span class="k">let</span> <span class="n">query</span> <span class="o">=</span> <span class="k">match</span> <span class="n">args</span><span class="nf">.next</span><span class="p">()</span> <span class="p">{</span>
            <span class="nf">Some</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">arg</span><span class="p">,</span>
            <span class="nb">None</span> <span class="k">=&gt;</span> <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="s">"Didn't get a query string"</span><span class="p">),</span>
        <span class="p">};</span>
        <span class="k">let</span> <span class="n">filename</span> <span class="o">=</span> <span class="k">match</span> <span class="n">args</span><span class="nf">.next</span><span class="p">()</span> <span class="p">{</span>
            <span class="nf">Some</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">arg</span><span class="p">,</span>
            <span class="nb">None</span> <span class="k">=&gt;</span> <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="s">"Didn't get a file name"</span><span class="p">),</span>
        <span class="p">};</span>

        <span class="k">let</span> <span class="n">case_sensitive</span> <span class="o">=</span> <span class="nn">env</span><span class="p">::</span><span class="nf">var</span><span class="p">(</span><span class="s">"CASE_INSENSITIVE"</span><span class="p">)</span><span class="nf">.is_err</span><span class="p">();</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="n">Config</span> <span class="p">{</span> <span class="n">query</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">case_sensitive</span> <span class="p">})</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>每次 <code class="language-plaintext highlighter-rouge">next()</code> 返回 <code class="language-plaintext highlighter-rouge">Some</code> 就 <code class="language-plaintext highlighter-rouge">match</code> 取出 <code class="language-plaintext highlighter-rouge">String</code>，返回 <code class="language-plaintext highlighter-rouge">None</code> 说明参数不够，直接 <code class="language-plaintext highlighter-rouge">Err</code> 提前返回。因为迭代器把值交给我们，<code class="language-plaintext highlighter-rouge">String</code> 是<strong>移动</strong>进 <code class="language-plaintext highlighter-rouge">Config</code> 的——不再需要 <code class="language-plaintext highlighter-rouge">clone</code> 分配新内存。</p>

<h3 id="二search用适配器替代可变-vector">二、<code class="language-plaintext highlighter-rouge">search</code>：用适配器替代可变 vector</h3>

<p>原版 <code class="language-plaintext highlighter-rouge">search</code> 里有个 <code class="language-plaintext highlighter-rouge">let mut results = Vec::new();</code> 加上 <code class="language-plaintext highlighter-rouge">for</code>/<code class="language-plaintext highlighter-rouge">push</code>。用 <code class="language-plaintext highlighter-rouge">filter</code> + <code class="language-plaintext highlighter-rouge">collect</code> 之后，整个函数的意图变成一句话：</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">pub</span> <span class="k">fn</span> <span class="n">search</span><span class="o">&lt;</span><span class="nv">'a</span><span class="o">&gt;</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">contents</span><span class="p">:</span> <span class="o">&amp;</span><span class="nv">'a</span> <span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Vec</span><span class="o">&lt;&amp;</span><span class="nv">'a</span> <span class="nb">str</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="n">contents</span><span class="nf">.lines</span><span class="p">()</span>
        <span class="nf">.filter</span><span class="p">(|</span><span class="n">line</span><span class="p">|</span> <span class="n">line</span><span class="nf">.contains</span><span class="p">(</span><span class="n">query</span><span class="p">))</span>
        <span class="nf">.collect</span><span class="p">()</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>for + push 版</th>
      <th>filter + collect 版</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>可变状态</td>
      <td>有一个 <code class="language-plaintext highlighter-rouge">results</code> vector</td>
      <td>无</td>
    </tr>
    <tr>
      <td>关注点</td>
      <td>如何遍历、如何攒结果</td>
      <td>只需表达”保留含 query 的行”</td>
    </tr>
    <tr>
      <td>未来并行化</td>
      <td>需管理 <code class="language-plaintext highlighter-rouge">results</code> 的并发访问</td>
      <td>无共享可变状态，更易改造</td>
    </tr>
  </tbody>
</table>

<p>函数式风格倾向于最小化可变状态，这会让未来的并行搜索更易实现。多数 Rust 开发者更偏好迭代器版本：初看略绕，习惯后反而<strong>更易看清代码目的</strong>——样板循环被抽走，只剩业务特有的过滤条件。</p>

<h2 id="性能对比循环一定更快吗">性能对比：循环一定更快吗？</h2>

<p>直觉上更”底层”的 <code class="language-plaintext highlighter-rouge">for</code> 循环应更快。用《福尔摩斯探案集》全文查找 “the” 做基准测试：</p>

<table>
  <thead>
    <tr>
      <th>实现</th>
      <th>耗时（ns/iter）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bench_search_for</code></td>
      <td>19,620,300（±915,700）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bench_search_iter</code></td>
      <td>19,234,900（±657,200）</td>
    </tr>
  </tbody>
</table>

<p>迭代器版本<strong>还略快一点</strong>。这正是 Rust 的<strong>零成本抽象</strong>（zero-cost abstractions）：抽象不引入运行时开销，与本贾尼·斯特劳斯特卢普在《Foundations of C++》中提出的零开销原则一致——”你不需要的，无需为其买单；你需要时，也找不到更好的代码了”。</p>

<p>书中还有一段音频解码器代码：用 <code class="language-plaintext highlighter-rouge">coefficients.iter().zip(&amp;buffer[i - 12..i]).map(...).sum::&lt;i64&gt;()</code> 做线性预测。其汇编与手写版相同——迭代次数固定为 12，Rust 直接<strong>展开</strong>（unroll）循环，系数放进寄存器，也没有数组边界检查。放心用迭代器和闭包：它们让代码更高级，却不因此变慢。</p>

<h2 id="实践建议与总结">实践建议与总结</h2>

<ol>
  <li><strong>闭包用于”延迟执行 + 捕获上下文”</strong>：要把行为存起来稍后执行、或让它记住几个外部变量时用它；一旦想复用”缓存结果”逻辑，就升级为 <code class="language-plaintext highlighter-rouge">Cacher</code> 式结构体（注意单值缓存、类型写死两个限制）。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Fn</code>/<code class="language-plaintext highlighter-rouge">FnMut</code>/<code class="language-plaintext highlighter-rouge">FnOnce</code> 先别背</strong>：从 <code class="language-plaintext highlighter-rouge">Fn</code> 写起，让编译器告诉你是否需要更强的约束；需要把值搬进新线程时再考虑 <code class="language-plaintext highlighter-rouge">move</code>。</li>
  <li><strong>优先迭代器风格，但先想清楚消费点</strong>：漏掉 <code class="language-plaintext highlighter-rouge">collect</code>/<code class="language-plaintext highlighter-rouge">sum</code> 这类消费适配器，整条链式调用等于没写（编译器会警告）；<code class="language-plaintext highlighter-rouge">iter</code>/<code class="language-plaintext highlighter-rouge">iter_mut</code>/<code class="language-plaintext highlighter-rouge">into_iter</code> 决定你拿到的是引用还是所有权，这直接决定要不要 <code class="language-plaintext highlighter-rouge">clone</code>。</li>
</ol>

<p>本章两个”意外收获”更值钱：一是抽象不必然付费——<code class="language-plaintext highlighter-rouge">filter().collect()</code> 与手写循环打平甚至更快；二是<strong>抽象收益会累积</strong>——上一章的 <code class="language-plaintext highlighter-rouge">clone</code> 与可变 <code class="language-plaintext highlighter-rouge">results</code> 能被干净消掉，正因它们被隔离在 <code class="language-plaintext highlighter-rouge">Config::new</code> 和 <code class="language-plaintext highlighter-rouge">search</code> 两个小函数里。下一章走出代码：<code class="language-plaintext highlighter-rouge">cargo</code> 的 profile 定制、文档注释与发布 crate。</p>]]></content><author><name>Wang Zhiyong</name><email>w1378379002@icloud.com</email></author><category term="Rust" /><category term="学习笔记" /><category term="TRPL" /><summary type="html"><![CDATA[Rust 学习笔记（14/21）：迭代器与闭包——Rust 的”零成本抽象”是怎么做到的]]></summary></entry></feed>